diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0412c6eacf..9029856167 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -22,6 +22,9 @@ android { resources.excludes.add("META-INF/LICENSE.md") resources.excludes.add("META-INF/NOTICE.md") } + androidResources { + generateLocaleConfig = true + } } configurations.all { @@ -87,6 +90,7 @@ dependencies { implementation(projects.libs.crypto) implementation(projects.libs.auth) implementation(projects.libs.blockchainSdk) + implementation(projects.libs.tangemSdkApi) implementation(projects.data.appCurrency) implementation(projects.data.appTheme) @@ -143,6 +147,10 @@ dependencies { implementation(projects.features.walletSettings.impl) implementation(projects.features.markets.api) implementation(projects.features.markets.impl) + implementation(projects.features.onramp.api) + implementation(projects.features.onramp.impl) + implementation(projects.features.onboardingV2.api) + implementation(projects.features.onboardingV2.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 4b3278bc9f..3ff0cba81e 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -1,16 +1,22 @@ package com.tangem.common import android.Manifest +import android.content.Context +import android.util.Log import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.test.espresso.intent.Intents import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule import com.kaspersky.components.composesupport.config.withComposeSupport import com.kaspersky.kaspresso.kaspresso.Kaspresso import com.kaspersky.kaspresso.testcases.api.testcase.TestCase +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.tap.MainActivity import com.tangem.tap.domain.sdk.TangemSdkManager import dagger.hilt.android.testing.HiltAndroidRule +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.rules.RuleChain import org.junit.runner.RunWith @@ -24,6 +30,9 @@ abstract class BaseTestCase : TestCase( @Inject lateinit var tangemSdkManager: TangemSdkManager + @Inject + lateinit var appPreferencesStore: AppPreferencesStore + @get:Rule open val composeTestRule = createAndroidComposeRule() @@ -48,9 +57,22 @@ abstract class BaseTestCase : TestCase( hiltRule.inject() Intents.init() additionalBeforeSection() + + runBlocking { + delay(INIT_DELAY) + } + }.after { + runBlocking { + appPreferencesStore.editData { prefs -> prefs.clear() } + } + additionalAfterSection() Intents.release() } + companion object { + private const val INIT_DELAY = 1000L + } + } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsScreenTest.kt index 8c4bd6d3c9..dd86a046cc 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsScreenTest.kt @@ -143,9 +143,6 @@ class DetailsScreenTest : BaseTestCase() { } } ComposeScreen.onComposeScreen(composeTestRule) { - step("Assert Link more cards button does not exist") { - linkMoreCardsButton.assertIsNotDisplayed() - } step("Assert Card Settings button is visible") { cardSettingsButton.assertIsDisplayed() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt index 9eb88b0075..eab4389f6b 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/StoriesTest.kt @@ -1,6 +1,7 @@ package com.tangem.tests import android.content.Intent.ACTION_VIEW +import androidx.test.espresso.intent.matcher.UriMatchers import com.tangem.common.BaseTestCase import com.tangem.common.extensions.clickWithAssertion import com.tangem.screens.DisclaimerTestScreen @@ -9,6 +10,7 @@ import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL import dagger.hilt.android.testing.HiltAndroidTest import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.kakao.intent.KIntent +import org.hamcrest.Matchers import org.junit.Test @HiltAndroidTest @@ -29,7 +31,7 @@ class StoriesTest : BaseTestCase() { step("Assert: browser opened") { val expectedIntent = KIntent { hasAction(ACTION_VIEW) - hasData(NEW_BUY_WALLET_URL) + hasData { toString().startsWith(NEW_BUY_WALLET_URL) } } expectedIntent.intended() device.uiDevice.pressBack() diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index cf01c91626..adbdabe422 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit cf01c91626bf58bdf6895c59a706b83c37a9e345 +Subproject commit adbdabe422b0513640d1750f276298683d342f61 diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json index 3badefc007..30fa60f3c4 100644 --- a/app/src/main/assets/testnet_tokens.json +++ b/app/src/main/assets/testnet_tokens.json @@ -748,6 +748,16 @@ "networkId": "core/test" } ] + }, + { + "id": "casper-network", + "name": "Casper", + "symbol": "CSPR", + "networks": [ + { + "networkId": "casper-network/test" + } + ] } ] } diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 7d37eacaf5..b116caf952 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -8,9 +8,9 @@ import com.tangem.core.analytics.filter.OneTimeEventFilter import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener -import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.GetAppThemeModeUseCase @@ -19,21 +19,19 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.feedback.GetCardInfoUseCase -import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.usercountry.GetUserCountryUseCase -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles +import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles import com.tangem.tap.proxy.AppStateHolder import dagger.hilt.EntryPoint @@ -50,7 +48,7 @@ interface ApplicationEntryPoint { fun getAppStateHolder(): AppStateHolder - fun getAssetLoader(): AssetLoader + fun getIssuersConfigStorage(): IssuersConfigStorage fun getFeatureTogglesManager(): FeatureTogglesManager @@ -60,18 +58,12 @@ interface ApplicationEntryPoint { fun getWalletConnect2Repository(): WalletConnect2Repository - fun getWalletConnectSessionsRepository(): WalletConnectSessionsRepository - fun getScanCardProcessor(): ScanCardProcessor fun getAppCurrencyRepository(): AppCurrencyRepository fun getWalletManagersFacade(): WalletManagersFacade - fun getNetworksRepository(): NetworksRepository - - fun getCurrenciesRepository(): CurrenciesRepository - fun getAppThemeModeRepository(): AppThemeModeRepository fun getBalanceHidingRepository(): BalanceHidingRepository @@ -102,8 +94,6 @@ interface ApplicationEntryPoint { fun getSendFeedbackEmailUseCase(): SendFeedbackEmailUseCase - fun getSaveBlockchainErrorUseCase(): SaveBlockchainErrorUseCase - fun getGetCardInfoUseCase(): GetCardInfoUseCase fun getUrlOpener(): UrlOpener @@ -119,4 +109,8 @@ interface ApplicationEntryPoint { fun getHomeFeatureToggles(): HomeFeatureToggles fun getGetUserCountryCodeUseCase(): GetUserCountryUseCase + + fun getOnrampFeatureToggles(): OnrampFeatureToggles + + fun getOnboardingV2FeatureToggles(): OnboardingV2FeatureToggles } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 169f30bc6c..cc4284cc7a 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -56,10 +56,10 @@ import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRou import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter -import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.operations.backup.BackupService +import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.extensions.init import com.tangem.tap.common.ActivityResultCallbackHolder import com.tangem.tap.common.DialogManager @@ -69,7 +69,6 @@ import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.showFragmentAllowingStateLoss import com.tangem.tap.common.redux.NotificationsHandler -import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.features.intentHandler.IntentProcessor import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler @@ -113,9 +112,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac lateinit var appStateHolder: AppStateHolder /** Router for opening tester menu */ - @Inject - lateinit var testerRouter: TesterRouter - @Inject lateinit var cardSdkOwner: CardSdkOwner @@ -309,17 +305,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac store.dispatch( DaggerGraphAction.SetActivityDependencies( - testerRouter = testerRouter, scanCardUseCase = scanCardUseCase, - walletRouter = walletRouter, walletConnectInteractor = walletConnectInteractor, - tokenDetailsRouter = tokenDetailsRouter, cardSdkConfigRepository = cardSdkConfigRepository, - sendRouter = sendRouter, - qrScanningRouter = qrScanningRouter, - emailSender = emailSender, - stakingRouter = stakingRouter, - pushNotificationsRouter = pushNotificationsRouter, ), ) } diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 0b5a37f14d..0e2e83f67f 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -20,10 +20,10 @@ import com.tangem.core.analytics.models.Basic.TransactionSent.WalletForm import com.tangem.core.featuretoggle.manager.FeatureTogglesManager import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.common.createNetworkLoggingInterceptor -import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.GetAppThemeModeUseCase @@ -33,18 +33,17 @@ import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.LogConfig import com.tangem.domain.feedback.GetCardInfoUseCase -import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.usercountry.GetUserCountryUseCase -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase +import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles +import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler @@ -53,10 +52,8 @@ import com.tangem.tap.common.images.createCoilImageLoader 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.common.redux.global.GlobalAction import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.tasks.product.DerivationsFinder -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphState @@ -85,8 +82,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val environmentConfigStorage: EnvironmentConfigStorage get() = entryPoint.getEnvironmentConfigStorage() - private val assetLoader: AssetLoader - get() = entryPoint.getAssetLoader() + private val issuersConfigStorage: IssuersConfigStorage + get() = entryPoint.getIssuersConfigStorage() private val featureTogglesManager: FeatureTogglesManager get() = entryPoint.getFeatureTogglesManager() @@ -100,9 +97,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val walletConnect2Repository: WalletConnect2Repository get() = entryPoint.getWalletConnect2Repository() - private val walletConnectSessionsRepository: WalletConnectSessionsRepository - get() = entryPoint.getWalletConnectSessionsRepository() - private val scanCardProcessor: ScanCardProcessor get() = entryPoint.getScanCardProcessor() @@ -112,12 +106,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val walletManagersFacade: WalletManagersFacade get() = entryPoint.getWalletManagersFacade() - private val networksRepository: NetworksRepository - get() = entryPoint.getNetworksRepository() - - private val currenciesRepository: CurrenciesRepository - get() = entryPoint.getCurrenciesRepository() - private val appThemeModeRepository: AppThemeModeRepository get() = entryPoint.getAppThemeModeRepository() @@ -163,9 +151,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase get() = entryPoint.getSendFeedbackEmailUseCase() - private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase - get() = entryPoint.getSaveBlockchainErrorUseCase() - private val getCardInfoUseCase: GetCardInfoUseCase get() = entryPoint.getGetCardInfoUseCase() @@ -189,6 +174,12 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val getUserCountryUseCase: GetUserCountryUseCase get() = entryPoint.getGetUserCountryCodeUseCase() + + private val onrampFeatureToggles: OnrampFeatureToggles + get() = entryPoint.getOnrampFeatureToggles() + + private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles + get() = entryPoint.getOnboardingV2FeatureToggles() // endregion override fun onCreate() { @@ -211,10 +202,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { runBlocking { featureTogglesManager.init() - val config = environmentConfigStorage.initialize() - store.dispatch(GlobalAction.SetConfigManager(environmentConfigStorage)) - - initWithConfigDependency(environmentConfig = config) + initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) } loadNativeLibraries() @@ -244,13 +232,10 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { networkConnectionManager = networkConnectionManager, cardScanningFeatureToggles = cardScanningFeatureToggles, walletConnectRepository = walletConnect2Repository, - walletConnectSessionsRepository = walletConnectSessionsRepository, scanCardProcessor = scanCardProcessor, appCurrencyRepository = appCurrencyRepository, walletManagersFacade = walletManagersFacade, appStateHolder = appStateHolder, - networksRepository = networksRepository, - currenciesRepository = currenciesRepository, appThemeModeRepository = appThemeModeRepository, balanceHidingRepository = balanceHidingRepository, walletsRepository = walletsRepository, @@ -259,19 +244,20 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase, generateWalletNameUseCase = generateWalletNameUseCase, cardRepository = cardRepository, - tangemSdkLogger = tangemSdkLogger, settingsRepository = settingsRepository, blockchainSDKFactory = blockchainSDKFactory, - saveBlockchainErrorUseCase = saveBlockchainErrorUseCase, sendFeedbackEmailUseCase = sendFeedbackEmailUseCase, getCardInfoUseCase = getCardInfoUseCase, - assetLoader = assetLoader, + issuersConfigStorage = issuersConfigStorage, urlOpener = urlOpener, shareManager = shareManager, appRouter = appRouter, transactionSignerFactory = transactionSignerFactory, homeFeatureToggles = homeFeatureToggles, getUserCountryUseCase = getUserCountryUseCase, + onrampFeatureToggles = onrampFeatureToggles, + environmentConfigStorage = environmentConfigStorage, + onboardingV2FeatureToggles = onboardingV2FeatureToggles, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt index 56c286492d..bf0bdc539a 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Token.kt @@ -1,6 +1,7 @@ package com.tangem.tap.common.analytics.events import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM /** [REDACTED_AUTHOR] @@ -17,7 +18,12 @@ sealed class Token( params: Map = mapOf(), ) : Token("Token / Receive", event, params) { - class ScreenOpened : Receive("Receive Screen Opened") + class ScreenOpened( + val token: String, + ) : Receive( + event = "Receive Screen Opened", + params = mapOf(TOKEN_PARAM to token), + ) class ButtonCopyAddress : Receive("Button - Copy Address") class ButtonShareAddress : Receive("Button - Share Address") } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index 27fc3c3bc3..b86d13b6e9 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -43,8 +43,8 @@ fun Store<*>.dispatchNotification(resId: Int) { dispatchOnMain(GlobalAction.ShowNotification(resId)) } -suspend fun Store.onUserWalletSelected(userWallet: UserWallet, sendAnalyticsEvent: Boolean = false) { - state.globalState.tapWalletManager.onWalletSelected(userWallet, sendAnalyticsEvent) +suspend fun Store.onUserWalletSelected(userWallet: UserWallet) { + state.globalState.tapWalletManager.onWalletSelected(userWallet) } fun Store<*>.dispatchErrorNotification(error: TapError) { diff --git a/app/src/main/java/com/tangem/tap/common/feature/Feature.kt b/app/src/main/java/com/tangem/tap/common/feature/Feature.kt deleted file mode 100644 index 1eb07546b4..0000000000 --- a/app/src/main/java/com/tangem/tap/common/feature/Feature.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.tap.common.feature - -/** -[REDACTED_AUTHOR] - */ -interface Feature { - fun featureIsSwitchedOn(): Boolean -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index a9525dc46c..740f5bd460 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -2,7 +2,6 @@ package com.tangem.tap.common.redux.global import com.tangem.common.CompletionResult import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.StateDialog @@ -74,8 +73,6 @@ sealed class GlobalAction : Action { data class IsSignWithRing(val isSignWithRing: Boolean) : GlobalAction() - data class SetConfigManager(val environmentConfigStorage: EnvironmentConfigStorage) : GlobalAction() - object ExchangeManager : GlobalAction() { object Init : GlobalAction() { data class Success( diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt index a0c16d69f6..c4366e8215 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt @@ -12,7 +12,6 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState -import com.tangem.tap.network.exchangeServices.BuyExchangeService import com.tangem.tap.network.exchangeServices.CardExchangeRules import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.ExchangeService @@ -56,8 +55,7 @@ private fun handleAction(action: Action, appState: () -> AppState?) { restoreAppCurrency() } is GlobalAction.ExchangeManager.Init -> { - val appStateSafe = appState() ?: return - val config = appStateSafe.globalState.environmentConfigStorage?.getConfigSync() ?: return + val config = store.inject(DaggerGraphState::environmentConfigStorage).getConfigSync() scope.launch { val scanResponseProvider: () -> ScanResponse? = { @@ -149,15 +147,10 @@ private fun makeSellExchangeService(environmentConfig: EnvironmentConfig): Excha } private fun makeBuyExchangeService(environmentConfig: EnvironmentConfig): ExchangeService { - return BuyExchangeService( - mercuryoService = makeMercuryoExchangeService(environmentConfig), + return MercuryoService( + environment = MercuryoEnvironment.prod( + widgetId = environmentConfig.mercuryoWidgetId, + secret = environmentConfig.mercuryoSecret, + ), ) -} - -private fun makeMercuryoExchangeService(environmentConfig: EnvironmentConfig): MercuryoService { - val mercuryoEnvironment = MercuryoEnvironment.prod( - environmentConfig.mercuryoWidgetId, - environmentConfig.mercuryoSecret, - ) - return MercuryoService(mercuryoEnvironment) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 6884603331..d55f99d21b 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -47,9 +47,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde is GlobalAction.RestoreAppCurrency.Success -> { globalState.copy(appCurrency = action.appCurrency) } - is GlobalAction.SetConfigManager -> { - globalState.copy(environmentConfigStorage = action.environmentConfigStorage) - } is GlobalAction.UpdateWalletSignedHashes -> { val card = globalState.scanResponse?.card ?: return globalState val wallet = card.wallets diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 0317eaaca6..b75d7370d8 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -1,6 +1,5 @@ package com.tangem.tap.common.redux.global -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.models.scan.ScanResponse @@ -16,7 +15,6 @@ data class GlobalState( val onboardingState: OnboardingState = OnboardingState(), val cardVerifiedOnline: Boolean = false, val tapWalletManager: TapWalletManager = TapWalletManager(), - val environmentConfigStorage: EnvironmentConfigStorage? = null, val appCurrency: AppCurrency = AppCurrency.Default, val scanCardFailsCounter: Int = 0, val dialog: StateDialog? = null, diff --git a/app/src/main/java/com/tangem/tap/common/ui/AddressInfoBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/AddressInfoBottomSheetDialog.kt index 6a2a14d073..8b2cb093dd 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/AddressInfoBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/AddressInfoBottomSheetDialog.kt @@ -38,7 +38,7 @@ internal class AddressInfoBottomSheetDialog( override fun show() { super.show() - Analytics.send(Token.Receive.ScreenOpened()) + Analytics.send(Token.Receive.ScreenOpened(stateDialog.currency.currencySymbol)) showData(data = stateDialog.addressData) } diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index 62f28c043e..c8d8c5c2bf 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -6,8 +6,8 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository +import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository -import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.network.exchangeServices.DefaultRampManager import com.tangem.tap.proxy.AppStateHolder import dagger.Module 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 c6118d1ac1..4754c5ec8e 100644 --- a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt @@ -3,9 +3,9 @@ package com.tangem.tap.di import android.content.Context import com.tangem.domain.card.BuildConfig import com.tangem.domain.card.repository.CardSdkConfigRepository +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.sdk.TangemSdkManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt b/app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt index b87e417fab..66d63389c9 100644 --- a/app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/CardDataModule.kt @@ -2,7 +2,7 @@ package com.tangem.tap.di.data import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.repository.DerivationsRepository -import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.card.DefaultDerivationsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 24c41445ab..55fb95de26 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -9,9 +9,9 @@ import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase +import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase import com.tangem.tap.domain.card.DefaultResetCardUseCase -import com.tangem.tap.domain.sdk.TangemSdkManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 51ed0f3dd5..8ec26eb230 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -12,7 +12,7 @@ import com.tangem.domain.settings.repositories.PromoSettingsRepository import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase import com.tangem.domain.settings.usercountry.GetUserCountryUseCase -import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository import dagger.Module import dagger.Provides diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index be10ba1059..53e9546b1b 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -1,10 +1,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.staking.* -import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository -import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.staking.repositories.StakingTransactionHashRepository +import com.tangem.domain.staking.repositories.* import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -51,6 +48,32 @@ internal object StakingDomainModule { ) } + @Provides + @Singleton + fun provideFetchActionsUseCase( + stakingRepository: StakingRepository, + stakingActionRepository: StakingActionRepository, + stakingErrorResolver: StakingErrorResolver, + ): FetchActionsUseCase { + return FetchActionsUseCase( + stakingRepository = stakingRepository, + stakingActionRepository = stakingActionRepository, + stakingErrorResolver = stakingErrorResolver, + ) + } + + @Provides + @Singleton + fun provideGetActionsUseCase( + stakingActionRepository: StakingActionRepository, + stakingErrorResolver: StakingErrorResolver, + ): GetActionsUseCase { + return GetActionsUseCase( + stakingActionRepository = stakingActionRepository, + stakingErrorResolver = stakingErrorResolver, + ) + } + @Provides @Singleton fun provideGetStakingTokensUseCase( @@ -123,38 +146,12 @@ internal object StakingDomainModule { ) } - @Provides - @Singleton - fun provideSavePendingTransactionUseCase( - stakingPendingTransactionRepository: StakingPendingTransactionRepository, - stakingErrorResolver: StakingErrorResolver, - ): SavePendingTransactionUseCase { - return SavePendingTransactionUseCase( - stakingPendingTransactionRepository = stakingPendingTransactionRepository, - stakingErrorResolver = stakingErrorResolver, - ) - } - @Provides @Singleton fun provideInvalidatePendingTransactionsUseCase( - stakingPendingTransactionRepository: StakingPendingTransactionRepository, stakingErrorResolver: StakingErrorResolver, ): InvalidatePendingTransactionsUseCase { return InvalidatePendingTransactionsUseCase( - stakingPendingTransactionRepository = stakingPendingTransactionRepository, - stakingErrorResolver = stakingErrorResolver, - ) - } - - @Provides - @Singleton - fun provideGetPendingTransactionsUseCase( - stakingPendingTransactionRepository: StakingPendingTransactionRepository, - stakingErrorResolver: StakingErrorResolver, - ): GetStakingPendingTransactionsUseCase { - return GetStakingPendingTransactionsUseCase( - stakingPendingTransactionRepository = stakingPendingTransactionRepository, stakingErrorResolver = stakingErrorResolver, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 545a7a7fe5..7e547182ce 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -7,7 +7,6 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.swap.domain.api.SwapRepository -import com.tangem.features.markets.MarketsFeatureToggles import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -68,17 +67,6 @@ internal object TokensDomainModule { ) } - @Provides - @Singleton - fun provideGetCardTokensListUseCase( - currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, - stakingRepository: StakingRepository, - ): GetNodlTokenListUseCase { - return GetNodlTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, stakingRepository) - } - @Provides @Singleton fun provideRemoveCurrencyUseCase( @@ -234,7 +222,6 @@ internal object TokensDomainModule { networksRepository: NetworksRepository, stakingRepository: StakingRepository, stakingFeatureToggles: StakingFeatureToggles, - marketsFeatureToggles: MarketsFeatureToggles, dispatchers: CoroutineDispatcherProvider, ): GetCryptoCurrencyActionsUseCase { return GetCryptoCurrencyActionsUseCase( @@ -246,7 +233,6 @@ internal object TokensDomainModule { networksRepository = networksRepository, stakingRepository = stakingRepository, stakingFeatureToggles = stakingFeatureToggles, - marketsFeatureToggles = marketsFeatureToggles, dispatchers = dispatchers, ) } @@ -287,6 +273,16 @@ internal object TokensDomainModule { ) } + @Provides + @Singleton + fun provideGetMinimumTransactionAmountSyncUseCase( + currencyChecksRepository: CurrencyChecksRepository, + ): GetMinimumTransactionAmountSyncUseCase { + return GetMinimumTransactionAmountSyncUseCase( + currencyChecksRepository = currencyChecksRepository, + ) + } + @Provides @Singleton fun provideIsCryptoCurrencyCoinCouldHideUseCase( diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index faaa017b70..4a7fac7d10 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -3,7 +3,6 @@ package com.tangem.tap.domain import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.Wallet import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.Basic import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.wallets.models.UserWallet import com.tangem.operations.attestation.Attestation @@ -28,19 +27,16 @@ class TapWalletManager( field = value } - suspend fun onWalletSelected(userWallet: UserWallet, sendAnalyticsEvent: Boolean) { + suspend fun onWalletSelected(userWallet: UserWallet) { // If a previous job was running, it gets cancelled before the new one starts, // ensuring that only one job is active at any given time. loadUserWalletDataJob = CoroutineScope(dispatchers.io) - .launch { loadUserWalletData(userWallet, sendAnalyticsEvent) } + .launch { loadUserWalletData(userWallet) } .also { it.join() } } - private suspend fun loadUserWalletData(userWallet: UserWallet, sendAnalyticsEvent: Boolean) { + private suspend fun loadUserWalletData(userWallet: UserWallet) { Analytics.setContext(userWallet.scanResponse) - if (sendAnalyticsEvent) { - Analytics.send(Basic.WalletOpened()) - } val scanResponse = userWallet.scanResponse val card = scanResponse.card val attestationFailed = card.attestation.status == Attestation.Status.Failed diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt index 8834604935..9a89db05b8 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDeleteSavedAccessCodesUseCase.kt @@ -6,7 +6,7 @@ import arrow.core.right import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.domain.card.DeleteSavedAccessCodesUseCase -import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.sdk.api.TangemSdkManager internal class DefaultDeleteSavedAccessCodesUseCase( private val tangemSdkManager: TangemSdkManager, diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt index 0013049c04..45cc75e9da 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt @@ -20,7 +20,7 @@ import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap -import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt index 1e322aeaba..aec5e21d16 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt @@ -14,7 +14,7 @@ import com.tangem.domain.card.ResetCardUseCase import com.tangem.domain.card.ResetCardUserCodeParams import com.tangem.domain.card.models.ResetCardError import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.sdk.api.TangemSdkManager internal class DefaultResetCardUseCase( private val tangemSdkManager: TangemSdkManager, diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt index ee417278e9..1a4b50cc5a 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt @@ -3,7 +3,7 @@ package com.tangem.tap.domain.scanCard.repository import com.tangem.common.CompletionResult import com.tangem.domain.card.repository.ScanCardRepository import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter // TODO: Move to the :data:card module 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 12c23b6fec..59dea35070 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 @@ -32,8 +32,9 @@ import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.sdk.api.CreateProductWalletTaskResponse +import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.derivationsFinder -import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.domain.tasks.product.* import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index faad2661c4..471a9436e2 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -20,9 +20,9 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.wallet.CreateWalletResponse -import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.sdk.api.CreateProductWalletTaskResponse +import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.sdk.mocks.MockProvider -import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse @Suppress("TooManyFunctions") class MockTangemSdkManager( diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockContent.kt index bb8ca7deab..3c5ab7756f 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockContent.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockContent.kt @@ -6,7 +6,7 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.wallet.CreateWalletResponse -import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse +import com.tangem.sdk.api.CreateProductWalletTaskResponse interface MockContent { diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt index 3811371aa7..83df27ebb4 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt @@ -4,10 +4,10 @@ import com.tangem.common.CompletionResult import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.domain.models.scan.ProductType +import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.tap.domain.sdk.mocks.content.NoteMockContent import com.tangem.tap.domain.sdk.mocks.content.WalletMockContent import com.tangem.tap.domain.sdk.mocks.content.Wallet2MockContent -import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse object MockProvider { diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/NoteMockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/NoteMockContent.kt index de2da2c546..3c7a2ae6bc 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/NoteMockContent.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/NoteMockContent.kt @@ -8,8 +8,8 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.operations.attestation.Attestation import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.tap.domain.sdk.mocks.MockContent -import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse import java.util.Date object NoteMockContent : MockContent { diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt index 4920acf082..93a6fcbeaa 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/Wallet2MockContent.kt @@ -13,8 +13,8 @@ import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.tap.domain.sdk.mocks.MockContent -import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse import java.util.Date object Wallet2MockContent : MockContent { diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt index 6168bdda1f..22263344cf 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/content/WalletMockContent.kt @@ -13,8 +13,8 @@ import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.tap.domain.sdk.mocks.MockContent -import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse import java.util.Date object WalletMockContent : MockContent { diff --git a/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt b/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt index 6a84f8cd30..e074ddc8f8 100644 --- a/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/settings/DefaultLegacySettingsRepository.kt @@ -1,7 +1,7 @@ package com.tangem.tap.domain.settings import com.tangem.domain.settings.repositories.LegacySettingsRepository -import com.tangem.tap.domain.sdk.TangemSdkManager +import com.tangem.sdk.api.TangemSdkManager internal class DefaultLegacySettingsRepository( private val tangemSdkManager: TangemSdkManager, diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index 699e7a527e..198afbd041 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -2,7 +2,6 @@ package com.tangem.tap.domain.tasks.product import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult -import com.tangem.common.card.Card import com.tangem.common.card.EllipticCurve import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.CardSession @@ -19,33 +18,15 @@ import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.models.scan.KeyWalletPublicKey -import com.tangem.operations.CommandResponse import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.backup.StartPrimaryCardLinkingCommand import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask -import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.operations.read.ReadWalletsListCommand import com.tangem.operations.wallet.CreateWalletTask +import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.tap.features.demo.DemoHelper import com.tangem.operations.wallet.CreateWalletResponse as SdkCreateWalletResponse -data class CreateProductWalletTaskResponse( - val card: CardDTO, - val derivedKeys: Map = mapOf(), - val primaryCard: PrimaryCard? = null, -) : CommandResponse { - constructor( - card: Card, - derivedKeys: Map = mapOf(), - primaryCard: PrimaryCard? = null, - ) : this( - card = CardDTO(card), - derivedKeys = derivedKeys, - primaryCard = primaryCard, - ) -} - private data class CreateWalletResponse( val cardId: String, val wallet: CardDTO.Wallet, diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt index 7f242bb845..beaf919a98 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt @@ -13,17 +13,19 @@ import com.tangem.tap.common.extensions.inject import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.tap.tangemSdkManager -import kotlinx.coroutines.flow.MutableStateFlow class TwinCardsManager(card: CardDTO) { + + private val issuersConfigStorage by lazy(mode = LazyThreadSafetyMode.NONE) { + store.inject(DaggerGraphState::issuersConfigStorage) + } + private val firstCardId: String = card.cardId private val publicKey: String = card.issuer.publicKey.toHexString() private var currentCardPublicKey: String? = null private var secondCardPublicKey: String? = null - private val issuerKeyPairFlow = MutableStateFlow(value = null) - suspend fun createFirstWallet(message: Message): CompletionResult { val response = tangemSdkManager.createFirstTwinWallet(cardId = firstCardId, initialMessage = message) @@ -70,23 +72,11 @@ class TwinCardsManager(card: CardDTO) { } private suspend fun getIssuerKeys(): KeyPair { - issuerKeyPairFlow.value?.let { return it } - - val assetLoader = store.inject(DaggerGraphState::assetLoader) - val issuer = assetLoader.loadList(fileName = ISSUERS_FILE_NAME) - .first { it.publicKey == publicKey } + val issuer = issuersConfigStorage.getConfig().first { it.publicKey == publicKey } return KeyPair( publicKey = issuer.publicKey.hexToBytes(), privateKey = issuer.privateKey.hexToBytes(), - ).also { - issuerKeyPairFlow.value = it - } + ) } - - private companion object { - const val ISSUERS_FILE_NAME = "tangem-app-config/issuers" - } -} - -private class Issuer(val privateKey: String, val publicKey: String) \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt index edddd1a886..07700916ac 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/TangemWcBlockchainHelper.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper +import com.tangem.tap.domain.walletconnect2.domain.models.Account import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles internal class TangemWcBlockchainHelper( @@ -23,6 +24,12 @@ internal class TangemWcBlockchainHelper( return blockchain?.toNetworkId() } + override fun chainIdsToBlockchains(chainIds: List): List { + return chainIds.mapNotNull { + it.parseId()?.chainIdToBlockchain() + }.distinct() + } + override fun chainIdToMissingNetworkNameOrNull(chainId: String): String? { val parsedId = chainId.parseId() ?: return null val blockchain = parsedId.chainIdToBlockchain() @@ -35,11 +42,12 @@ internal class TangemWcBlockchainHelper( } } - override fun networkIdToChainIdOrNull(networkId: String): String? { + override fun networkIdToChainIdOrNull(networkId: String): List { val blockchain = Blockchain.fromNetworkId(networkId) - val namespace = blockchain?.getCaip2Namespace() ?: return null - val chainId = blockchain.getCaip2ChainId() ?: return null - return "$namespace$CHAIN_SEPARATOR$chainId" + val namespace = blockchain?.getCaip2Namespace() ?: return emptyList() + return blockchain.getCaip2ChainIds().map { + "$namespace$CHAIN_SEPARATOR$it" + } } override fun getNamespaceFromFullChainIdOrNull(chainId: String): String? { @@ -52,8 +60,18 @@ internal class TangemWcBlockchainHelper( return Blockchain.fromNetworkId(networkId)?.fullName } - private fun Blockchain.getCaip2ChainId(): String? { - if (this.isEvm()) return this.getChainId()?.toString() + override fun chainIdsToAccounts( + walletAddress: String, + chainIds: List, + derivationPath: String?, + ): List { + return chainIds.map { chainId -> + Account(chainId, walletAddress, derivationPath) + } + } + + private fun Blockchain.getCaip2ChainIds(): List { + if (this.isEvm()) return listOfNotNull(this.getChainId()?.toString()) return when (this) { /* @@ -61,13 +79,12 @@ internal class TangemWcBlockchainHelper( * uncommented is used. * Docs: https://docs.walletconnect.com/advanced/multichain/chain-list * - * Blockchain.Solana -> "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" * */ - Blockchain.Solana -> "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ" - Blockchain.SolanaTestnet -> "z4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z" - Blockchain.Polkadot -> "91b171bb158e2d3848fa23a9f1c25182" - Blockchain.Tron -> "0x2b6653dc" - else -> null + Blockchain.Solana -> listOf("5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ") + Blockchain.SolanaTestnet -> listOf("z4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z") + Blockchain.Polkadot -> listOf("91b171bb158e2d3848fa23a9f1c25182") + Blockchain.Tron -> listOf("0x2b6653dc") + else -> emptyList() } } @@ -92,6 +109,7 @@ internal class TangemWcBlockchainHelper( second.toIntOrNull() ?.let(Blockchain::fromChainId) } + SOLANA_NAMESPACE -> Blockchain.Solana else -> { Blockchain.fromNetworkId(networkId = first) .takeIf(supportedNonEvmBlockchains::contains) @@ -101,6 +119,7 @@ internal class TangemWcBlockchainHelper( private companion object { const val EVM_NAMESPACE = "eip155" + const val SOLANA_NAMESPACE = "solana" const val CHAIN_SEPARATOR = ":" const val TESTNET_SEPARATOR = "/" } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt index 073fcb8f0d..d84263034d 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt @@ -19,7 +19,7 @@ internal class WalletConnectEventsHandlerImpl : WalletConnectEventsHandler { WalletConnectDialog.SessionProposalDialog( sessionProposal = proposal, networks = networksFormatted, - onApprove = { store.dispatchOnMain(WalletConnectAction.ApproveProposal) }, + onApprove = { store.dispatchOnMain(WalletConnectAction.ApproveProposal(proposal)) }, onReject = { store.dispatchOnMain(WalletConnectAction.RejectProposal) }, ), ), diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 8f80642201..9e980da2ba 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -207,6 +207,10 @@ internal class DefaultLegacyWalletConnectRepository( Timber.i("onSessionDelete: $sessionDelete") } + override fun onSessionExtend(session: Wallet.Model.Session) { + Timber.i("onSessionExtend: $session") + } + override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { // Triggered when wallet receives the session settlement response from Dapp Timber.i("onSessionSettleResponse: $settleSessionResponse") @@ -264,8 +268,6 @@ internal class DefaultLegacyWalletConnectRepository( } override fun approve(userNamespaces: Map>) { - this.userNamespaces = userNamespaces - val sessionProposal: Wallet.Model.SessionProposal = requireNotNull(this.sessionProposal) val userChains = userNamespaces.flatMap { namespace -> diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index 122a9ed76f..9d3e956799 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -102,7 +102,7 @@ class WalletConnectInteractor( private suspend fun setupUserChains(userWallet: UserWallet, currencies: List) { val accounts = getAccountsForWc( userWallet = userWallet, - networks = currencies.map { it.network }, + networks = currencies.map { it.network }.distinct(), ) setUserChains(accounts) handleDeeplinkStack(accounts) @@ -403,18 +403,14 @@ class WalletConnectInteractor( derivationPath = it.derivationPath.value, ) } - return walletManagers.mapNotNull { + return walletManagers.flatMap { val wallet = it.wallet - val chainId = blockchainHelper.networkIdToChainIdOrNull( - wallet.blockchain.toNetworkId(), + val chainIds = blockchainHelper.networkIdToChainIdOrNull(wallet.blockchain.toNetworkId()) + blockchainHelper.chainIdsToAccounts( + walletAddress = wallet.address, + chainIds = chainIds, + derivationPath = wallet.publicKey.derivationPath?.rawPath, ) - chainId?.let { - Account( - chainId, - wallet.address, - wallet.publicKey.derivationPath?.rawPath, - ) - } } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt index 489d086af0..91771ac6e1 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcBlockchainHelper.kt @@ -1,13 +1,20 @@ package com.tangem.tap.domain.walletconnect2.domain +import com.tangem.blockchain.common.Blockchain +import com.tangem.tap.domain.walletconnect2.domain.models.Account + interface WcBlockchainHelper { fun chainIdToNetworkIdOrNull(chainId: String): String? fun chainIdToMissingNetworkNameOrNull(chainId: String): String? - fun networkIdToChainIdOrNull(networkId: String): String? + fun networkIdToChainIdOrNull(networkId: String): List fun getNamespaceFromFullChainIdOrNull(chainId: String): String? fun chainIdToFullNameOrNull(chainId: String): String? + + fun chainIdsToAccounts(walletAddress: String, chainIds: List, derivationPath: String?): List + + fun chainIdsToBlockchains(chainIds: List): List } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt index 3cb1187509..7c937b3772 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcJrpcMethods.kt @@ -223,14 +223,14 @@ internal class WcJrpcRequestsDeserializer @Inject constructor(@SdkMoshi private WcRequest.AddChain(data = deserializedParams) } WcJrpcMethods.SOLANA_SIGN_TX -> { - val tx = moshi.adapter(SolanaTransactionRequest::class.java) + val tx = moshi.adapter(SolanaTransactionRequest::class.java) .fromJsonOrNull(params) ?: return customRequest WcRequest.SolanaSignRequest(data = tx) } WcJrpcMethods.SOLANA_SIGN_MESSAGE -> { - val signMessage = moshi.adapter(SolanaSignMessage::class.java) + val signMessage = moshi.adapter(SolanaSignMessage::class.java) .fromJsonOrNull(params) ?: return customRequest val data = WcSignMessage( diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt index e02d7cc679..367d7b6a9e 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WcSessionRequestConverter.kt @@ -1,12 +1,12 @@ package com.tangem.tap.domain.walletconnect2.domain import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper -import com.tangem.tap.domain.walletconnect2.domain.mapper.mapToTransaction import com.tangem.tap.domain.walletconnect2.domain.models.BnbData import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType +import okio.ByteString.Companion.decodeBase64 internal class WcSessionRequestConverter( private val blockchainHelper: WcBlockchainHelper, @@ -115,11 +115,11 @@ internal class WcSessionRequestConverter( ) } is WcRequest.SolanaSignRequest -> { - val data = request.data.mapToTransaction() + val transaction = request.data.transaction WcPreparedRequest.SignTransaction( preparedRequestData = WcGenericTransactionData( - hashToSign = data.getSerializedMessage(), + hashToSign = transaction.prepareSolanaTransaction(), dAppName = sessionRequest.metaName, type = TransactionType.SOLANA_TX, ), @@ -132,6 +132,16 @@ internal class WcSessionRequestConverter( } } + /** + * Input transaction in Base64 string + */ + private fun String.prepareSolanaTransaction(): ByteArray { + return this.decodeBase64() + ?.toByteArray() + ?.drop(SOLANA_SIGNATURE_PLACEHOLDER_LENGTH) + ?.toByteArray() ?: ByteArray(0) + } + private fun getWalletAddress(request: WcRequest): String? { return when (request) { is WcRequest.BnbTrade -> request.data.accountNumber @@ -156,4 +166,8 @@ internal class WcSessionRequestConverter( it.chainId == sessionRequest.chainId && it.walletAddress.lowercase() == walletAddress?.lowercase() }?.derivationPath } + + companion object { + private const val SOLANA_SIGNATURE_PLACEHOLDER_LENGTH = 65 + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt index 7ea66042f4..b77ecffae0 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/solana/SolanaTransactionRequest.kt @@ -14,6 +14,9 @@ data class SolanaTransactionRequest( @Json(name = "instructions") val instructions: List, + + @Json(name = "transaction") + val transaction: String, ) : WcRequestData { @JsonClass(generateAdapter = true) @@ -22,7 +25,7 @@ data class SolanaTransactionRequest( val programId: String, @Json(name = "data") - val data: List, + val data: String, @Json(name = "keys") val keys: List, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt index a6c5dabb19..06591c01b0 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.details.redux.walletconnect import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError +import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen import org.rekotlin.Action @@ -22,7 +23,7 @@ sealed class WalletConnectAction : Action { data class ShowClipboardOrScanQrDialog(val wcUri: String) : WalletConnectAction() //region WalletConnect 2.0 - data object ApproveProposal : WalletConnectAction() + data class ApproveProposal(val proposal: WalletConnectEvents.SessionProposal) : WalletConnectAction() data object RejectProposal : WalletConnectAction() data object SessionEstablished : WalletConnectAction() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index 6b0e0898ec..facfd60eab 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -13,7 +13,6 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest -import com.tangem.tap.domain.walletconnect2.domain.models.Account import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.proxy.redux.DaggerGraphState @@ -86,19 +85,21 @@ class WalletConnectMiddleware { } is WalletConnectAction.ApproveProposal -> { scope.launch { + val proposalChainIds = action.proposal.requiredChainIds + action.proposal.optionalChainIds + val proposalBlockchains = + walletConnectInteractor.blockchainHelper.chainIdsToBlockchains(proposalChainIds) val accounts = getWalletManagers() - .mapNotNull { + .filter { walletManager -> proposalBlockchains.contains(walletManager.wallet.blockchain) } + .flatMap { val wallet = it.wallet - val chainId = walletConnectInteractor.blockchainHelper.networkIdToChainIdOrNull( + val chainIds = walletConnectInteractor.blockchainHelper.networkIdToChainIdOrNull( wallet.blockchain.toNetworkId(), ) - chainId?.let { - Account( - chainId, - wallet.address, - wallet.publicKey.derivationPath?.rawPath, - ) - } + walletConnectInteractor.blockchainHelper.chainIdsToAccounts( + walletAddress = wallet.address, + chainIds = chainIds, + derivationPath = wallet.publicKey.derivationPath?.rawPath, + ) } walletConnectInteractor.approveSessionProposal(accounts) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index 9af2855279..8ede05a98a 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -18,13 +18,13 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.domain.extensions.signedHashesCount -import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor import com.tangem.tap.features.details.ui.common.utils.* import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt index d65fb74a54..1652506ca9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt @@ -6,10 +6,10 @@ import com.tangem.common.doOnSuccess import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor import com.tangem.tap.features.details.ui.common.utils.isAccessCodeRecoveryEnabled import com.tangem.tap.store diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt index a97cb68bfe..a7f0b763b3 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeViewModel.kt @@ -7,10 +7,10 @@ import com.tangem.common.core.TangemSdkError import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.features.details.redux.SecurityOption import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor import com.tangem.tap.features.details.ui.common.utils.getAllowedSecurityOptions diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index e9db3ee67e..444b2c6f89 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -6,7 +6,6 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.Basic import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase import com.tangem.domain.balancehiding.BalanceHidingSettings @@ -103,7 +102,6 @@ internal class MainViewModel @Inject constructor( .distinctUntilChanged() .onEach { userWallet -> Analytics.setContext(userWallet.scanResponse) - Analytics.send(Basic.WalletOpened()) } .flowOn(dispatchers.io) .launchIn(viewModelScope) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 35007254c1..2f18b059ca 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -66,6 +66,10 @@ object OnboardingHelper { } fun whereToNavigate(scanResponse: ScanResponse): AppRoute { + if (store.inject(DaggerGraphState::onboardingV2FeatureToggles).isOnboardingV2Enabled) { + return AppRoute.Onboarding(scanResponse) + } + return when (val type = scanResponse.productType) { ProductType.Note -> AppRoute.OnboardingNote ProductType.Wallet, diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt index e7cc9589a4..0f29d53f73 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt @@ -2,11 +2,11 @@ package com.tangem.tap.features.onboarding.products.wallet.redux import android.net.Uri import com.tangem.common.CompletionResult -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.common.card.Card +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.onboarding.data.model.CreateWalletResponse import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource -import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse +import com.tangem.sdk.api.CreateProductWalletTaskResponse import kotlinx.coroutines.CoroutineScope import org.rekotlin.Action diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 42ef53fa15..dc9e77a48d 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -28,13 +28,13 @@ import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSo import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator import com.tangem.operations.attestation.OnlineCardVerifier import com.tangem.operations.backup.BackupService +import com.tangem.sdk.api.CreateProductWalletTaskResponse import com.tangem.tap.* import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.home.redux.HomeAction import com.tangem.tap.features.onboarding.OnboardingDialog 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 index 2f26b2a39a..a21351a6b9 100644 --- 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 @@ -54,6 +54,15 @@ object TradeCryptoMiddleware { } private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) { + val isOnrampEnabled = store.inject(DaggerGraphState::onrampFeatureToggles).isFeatureEnabled + if (isOnrampEnabled) proceedWithOnramp() else proceedWithLegacyBuyAction(state, action) + } + + private fun proceedWithOnramp() { + store.dispatchNavigationAction { push(AppRoute.Onramp) } + } + + private fun proceedWithLegacyBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) { val networkAddress = action.cryptoCurrencyStatus.value.networkAddress ?.defaultAddress ?.let(NetworkAddress.Address::value) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt deleted file mode 100644 index a8861eed2f..0000000000 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.tap.network.exchangeServices - -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.tap.domain.model.Currency -import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService -import com.tangem.tap.scope -import kotlinx.coroutines.launch - -/** -[REDACTED_AUTHOR] - * Temporary wrapper for the buy services. Service switches based on selected product type. - */ -internal class BuyExchangeService( - private val mercuryoService: MercuryoService, -) : ExchangeService, ExchangeUrlBuilder { - - init { - scope.launch { - mercuryoService.update() - } - } - - private val currentService: ExchangeService = mercuryoService - - override suspend fun update() { - currentService.update() - } - - override fun featureIsSwitchedOn(): Boolean = currentService.featureIsSwitchedOn() - - override fun isBuyAllowed(): Boolean = currentService.isBuyAllowed() - - override fun isSellAllowed(): Boolean = currentService.isSellAllowed() - - override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = - currentService.availableForBuy(scanResponse, currency) - - override fun availableForSell(currency: Currency): Boolean = currentService.availableForSell(currency) - - override fun getUrl( - action: CurrencyExchangeManager.Action, - cryptoCurrency: CryptoCurrency, - fiatCurrencyName: String, - walletAddress: String, - isDarkTheme: Boolean, - ): String? { - return currentService.getUrl( - action, - cryptoCurrency, - fiatCurrencyName, - walletAddress, - isDarkTheme, - ) - } - - override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? { - return currentService.getSellCryptoReceiptUrl(action, transactionId) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt index 696f9288bc..0795969b5c 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt @@ -13,12 +13,6 @@ class CardExchangeRules( val cardProvider: () -> CardDTO?, ) : ExchangeRules { - override fun featureIsSwitchedOn(): Boolean { - val card = cardProvider() ?: return false - - return !card.isStart2Coin - } - override fun isBuyAllowed(): Boolean { val card = cardProvider() ?: return false diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index 2008fae3cc..71c317e080 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -30,8 +30,6 @@ class CurrencyExchangeManager( private val primaryRules: ExchangeRules, ) : ExchangeService { - override fun featureIsSwitchedOn(): Boolean = primaryRules.featureIsSwitchedOn() - override suspend fun update() { buyService.update() sellService.update() diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt index 1cfd8502ed..495bcec410 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt @@ -2,7 +2,6 @@ package com.tangem.tap.network.exchangeServices import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.tap.common.feature.Feature import com.tangem.tap.domain.model.Currency interface Exchanger { @@ -12,12 +11,11 @@ interface Exchanger { fun availableForSell(currency: Currency): Boolean } -interface ExchangeService : Feature, Exchanger, ExchangeUrlBuilder { +interface ExchangeService : Exchanger, ExchangeUrlBuilder { suspend fun update() companion object { fun dummy(): ExchangeService = object : ExchangeService { - override fun featureIsSwitchedOn(): Boolean = false override suspend fun update() {} override fun isBuyAllowed(): Boolean = false override fun isSellAllowed(): Boolean = false @@ -39,11 +37,10 @@ interface ExchangeService : Feature, Exchanger, ExchangeUrlBuilder { } } -interface ExchangeRules : Feature, Exchanger { +interface ExchangeRules : Exchanger { companion object { fun dummy(): ExchangeRules = object : ExchangeRules { - override fun featureIsSwitchedOn(): Boolean = false override fun isBuyAllowed(): Boolean = false override fun isSellAllowed(): Boolean = false override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean = false diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt new file mode 100644 index 0000000000..946d2afce1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoBlockchainMapping.kt @@ -0,0 +1,145 @@ +package com.tangem.tap.network.exchangeServices.mercuryo + +import com.tangem.blockchain.common.Blockchain + +/** + * Map [Blockchain] to ids from [link](https://api.mercuryo.io/v1.6/lib/currencies) [crypto_currencies] + */ +@Suppress("CyclomaticComplexMethod") +internal val Blockchain.mercuryoNetwork: String? + get() { + return when (this) { + Blockchain.Algorand -> "ALGORAND" + Blockchain.Arbitrum -> "ARBITRUM" + Blockchain.Avalanche -> "AVALANCHE" + Blockchain.BSC -> "BINANCESMARTCHAIN" + Blockchain.Bitcoin -> "BITCOIN" + Blockchain.BitcoinCash -> "BITCOINCASH" + Blockchain.Cardano -> "CARDANO" + Blockchain.Cosmos -> "COSMOS" + Blockchain.Dash -> "DASH" + Blockchain.Dogecoin -> "DOGECOIN" + Blockchain.Ethereum -> "ETHEREUM" + Blockchain.Fantom -> "FANTOM" + Blockchain.Kusama -> "KUSAMA" + Blockchain.Litecoin -> "LITECOIN" + Blockchain.Near -> "NEAR_PROTOCOL" + Blockchain.TON -> "NEWTON" + Blockchain.Optimism -> "OPTIMISM" + Blockchain.Polkadot -> "POLKADOT" + Blockchain.Polygon -> "POLYGON" + Blockchain.XRP -> "RIPPLE" + Blockchain.Solana -> "SOLANA" + Blockchain.Stellar -> "STELLAR" + Blockchain.Tezos -> "TEZOS" + Blockchain.Tron -> "TRON" + Blockchain.ZkSyncEra -> "ZKSYNC" + Blockchain.Base -> "BASE" + Blockchain.ArbitrumTestnet -> null + Blockchain.AvalancheTestnet -> null + Blockchain.Binance -> null + Blockchain.BinanceTestnet -> null + Blockchain.BSCTestnet -> null + Blockchain.BitcoinTestnet -> null + Blockchain.BitcoinCashTestnet -> null + Blockchain.CosmosTestnet -> null + Blockchain.Ducatus -> null + Blockchain.EthereumTestnet -> null + Blockchain.EthereumClassic -> null + Blockchain.EthereumClassicTestnet -> null + Blockchain.FantomTestnet -> null + Blockchain.NearTestnet -> null + Blockchain.PolkadotTestnet -> null + Blockchain.Kava -> null + Blockchain.KavaTestnet -> null + Blockchain.PolygonTestnet -> null + Blockchain.RSK -> null + Blockchain.Sei -> null + Blockchain.SeiTestnet -> null + Blockchain.StellarTestnet -> null + Blockchain.SolanaTestnet -> null + Blockchain.TronTestnet -> null + Blockchain.Gnosis -> null + Blockchain.OptimismTestnet -> null + Blockchain.Dischain -> null + Blockchain.EthereumPow -> null + Blockchain.EthereumPowTestnet -> null + Blockchain.Kaspa -> null + Blockchain.Telos -> null + Blockchain.TelosTestnet -> null + Blockchain.TONTestnet -> null + Blockchain.Ravencoin -> null + Blockchain.RavencoinTestnet -> null + Blockchain.TerraV1 -> null + Blockchain.TerraV2 -> null + Blockchain.Cronos -> null + Blockchain.AlephZero -> null + Blockchain.AlephZeroTestnet -> null + Blockchain.OctaSpace -> null + Blockchain.OctaSpaceTestnet -> null + Blockchain.Chia -> null + Blockchain.ChiaTestnet -> null + Blockchain.Decimal -> null + Blockchain.DecimalTestnet -> null + Blockchain.XDC -> null + Blockchain.XDCTestnet -> null + Blockchain.VeChain -> null + Blockchain.VeChainTestnet -> null + Blockchain.Aptos -> null + Blockchain.AptosTestnet -> null + Blockchain.Playa3ull -> null + Blockchain.Shibarium -> null + Blockchain.ShibariumTestnet -> null + Blockchain.AlgorandTestnet -> null + Blockchain.Hedera -> null + Blockchain.HederaTestnet -> null + Blockchain.Aurora -> null + Blockchain.AuroraTestnet -> null + Blockchain.Areon -> null + Blockchain.AreonTestnet -> null + Blockchain.PulseChain -> null + Blockchain.PulseChainTestnet -> null + Blockchain.ZkSyncEraTestnet -> null + Blockchain.Nexa -> null + Blockchain.NexaTestnet -> null + Blockchain.Moonbeam -> null + Blockchain.MoonbeamTestnet -> null + Blockchain.Manta -> null + Blockchain.MantaTestnet -> null + Blockchain.PolygonZkEVM -> null + Blockchain.PolygonZkEVMTestnet -> null + Blockchain.Radiant -> null + Blockchain.BaseTestnet -> null + Blockchain.Moonriver -> null + Blockchain.MoonriverTestnet -> null + Blockchain.Mantle -> null + Blockchain.MantleTestnet -> null + Blockchain.Flare -> null + Blockchain.FlareTestnet -> null + Blockchain.Taraxa -> null + Blockchain.TaraxaTestnet -> null + Blockchain.Koinos -> null + Blockchain.KoinosTestnet -> null + Blockchain.Joystream -> null + Blockchain.Bittensor -> null + Blockchain.Filecoin -> null + Blockchain.Blast -> null + Blockchain.BlastTestnet -> null + Blockchain.Cyber -> null + Blockchain.CyberTestnet -> null + Blockchain.InternetComputer -> null + Blockchain.Sui -> null + Blockchain.SuiTestnet -> null + Blockchain.EnergyWebChain -> null + Blockchain.EnergyWebChainTestnet -> null + Blockchain.EnergyWebX -> null + Blockchain.EnergyWebXTestnet -> null + Blockchain.Casper -> null + Blockchain.CasperTestnet -> null + Blockchain.Core -> null + Blockchain.CoreTestnet -> null + Blockchain.Unknown -> null + Blockchain.Xodex -> null + Blockchain.Canxium -> null + } + } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt index 8bcc8168e9..50b558907b 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt @@ -23,8 +23,6 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E private val availableMercuryoCurrencies = CopyOnWriteArrayList() - override fun featureIsSwitchedOn(): Boolean = true - override fun isBuyAllowed(): Boolean = true override fun isSellAllowed(): Boolean = false @@ -32,7 +30,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E override fun availableForBuy(scanResponse: ScanResponse, currency: Currency): Boolean { if (!isBuyAllowed()) return false - val mercuryoNetwork = currency.blockchain.mercuryoNetwork() + val mercuryoNetwork = currency.blockchain.mercuryoNetwork val contractAddress = (currency as? Currency.Token)?.token?.contractAddress ?: "" val availableCurrency = availableMercuryoCurrencies.firstOrNull { it.currencySymbol == currency.currencySymbol && @@ -79,7 +77,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E .appendQueryParameter("redirect_url", ExchangeUrlBuilder.SUCCESS_URL) if (isDarkTheme) builder.appendQueryParameter("theme", "1inch") - blockchain.mercuryoNetwork()?.let { + blockchain.mercuryoNetwork?.let { builder.appendQueryParameter("network", it) } @@ -93,37 +91,6 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E availableMercuryoCurrencies.addAll(data.config.cryptoCurrencies) } - @Suppress("CyclomaticComplexMethod") - private fun Blockchain.mercuryoNetwork(): String? { - return when (this) { - // Blockchain.Algorand -> "ALGORAND" //TODO: Uncomment with algo support - Blockchain.Arbitrum -> "ARBITRUM" - Blockchain.Avalanche -> "AVALANCHE" - Blockchain.BSC -> "BINANCESMARTCHAIN" - Blockchain.Bitcoin -> "BITCOIN" - Blockchain.BitcoinCash -> "BITCOINCASH" - Blockchain.Cardano -> "CARDANO" - Blockchain.Cosmos -> "COSMOS" - Blockchain.Dash -> "DASH" - Blockchain.Dogecoin -> "DOGECOIN" - Blockchain.Ethereum -> "ETHEREUM" - Blockchain.Fantom -> "FANTOM" - Blockchain.Kusama -> "KUSAMA" - Blockchain.Litecoin -> "LITECOIN" - Blockchain.Near -> "NEAR_PROTOCOL" - Blockchain.TON -> "NEWTON" - Blockchain.Optimism -> "OPTIMISM" - Blockchain.Polkadot -> "POLKADOT" - Blockchain.Polygon -> "POLYGON" - Blockchain.XRP -> "RIPPLE" - Blockchain.Solana -> "SOLANA" - Blockchain.Stellar -> "STELLAR" - Blockchain.Tezos -> "TEZOS" - Blockchain.Tron -> "TRON" - else -> null - } - } - private fun signature(address: String) = (address + environment.secret).calculateSha512().toHexString().lowercase() private companion object { diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/BlockchainExt.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/BlockchainExt.kt deleted file mode 100644 index c5f04b9039..0000000000 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/BlockchainExt.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.tap.network.exchangeServices.moonpay - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Blockchain.* -import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPaySupportedCurrency - -internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? - get() = when (this) { - Algorand -> MoonPaySupportedCurrency(networkCode = "algorand", currencyCode = "algo") - Aptos -> MoonPaySupportedCurrency(networkCode = "aptos", currencyCode = "apt") - Arbitrum -> MoonPaySupportedCurrency(networkCode = "arbitrum", currencyCode = "eth_arbitrum") - Avalanche -> MoonPaySupportedCurrency(networkCode = "avalanche_c_chain", currencyCode = "avax_cchain") - Binance -> MoonPaySupportedCurrency(networkCode = "bnb_chain", currencyCode = "bnb") - Bitcoin -> MoonPaySupportedCurrency(networkCode = "bitcoin", currencyCode = "btc") - BitcoinCash -> MoonPaySupportedCurrency(networkCode = "bitcoin_cash", currencyCode = "bch") - BSC -> MoonPaySupportedCurrency(networkCode = "binance_smart_chain", currencyCode = "bnb_bsc") - Cardano -> MoonPaySupportedCurrency(networkCode = "cardano", currencyCode = "ada") - Cosmos -> MoonPaySupportedCurrency(networkCode = "cosmos", currencyCode = "atom") - Dogecoin -> MoonPaySupportedCurrency(networkCode = "dogecoin", currencyCode = "doge") - Ethereum -> MoonPaySupportedCurrency(networkCode = "ethereum", currencyCode = "eth") - EthereumClassic -> MoonPaySupportedCurrency(networkCode = "ethereum_classic", currencyCode = "etc") - Hedera -> MoonPaySupportedCurrency(networkCode = "hedera", currencyCode = "hbar") - Litecoin -> MoonPaySupportedCurrency(networkCode = "litecoin", currencyCode = "ltc") - Near -> MoonPaySupportedCurrency(networkCode = "near", currencyCode = "near") - Optimism -> MoonPaySupportedCurrency(networkCode = "optimism", currencyCode = "eth_optimism") - Polkadot -> MoonPaySupportedCurrency(networkCode = "polkadot", currencyCode = "dot") - Polygon -> MoonPaySupportedCurrency(networkCode = "polygon", currencyCode = "matic_polygon") - Ravencoin -> MoonPaySupportedCurrency(networkCode = "ravencoin", currencyCode = "rvn") - Solana -> MoonPaySupportedCurrency(networkCode = "solana", currencyCode = "sol") - Stellar -> MoonPaySupportedCurrency(networkCode = "stellar", currencyCode = "xlm") - Tezos -> MoonPaySupportedCurrency(networkCode = "tezos", currencyCode = "xtz") - TON -> MoonPaySupportedCurrency(networkCode = "ton", currencyCode = "ton") - Tron -> MoonPaySupportedCurrency(networkCode = "tron", currencyCode = "trx") - VeChain -> MoonPaySupportedCurrency(networkCode = "vechain", currencyCode = "vet") - XRP -> MoonPaySupportedCurrency(networkCode = "ripple", currencyCode = "xrp") - else -> null - } \ 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 de34810420..26b26f0b61 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 @@ -32,8 +32,6 @@ class MoonPayService( private var status: MoonPayStatus? = null - override fun featureIsSwitchedOn(): Boolean = true - override suspend fun update() { withIOContext { performRequest { diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt new file mode 100644 index 0000000000..fb01fff927 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt @@ -0,0 +1,145 @@ +package com.tangem.tap.network.exchangeServices.moonpay + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Blockchain.* +import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPaySupportedCurrency + +/** + * Map [Blockchain] to [MoonPaySupportedCurrency.networkCode] and [MoonPaySupportedCurrency.currencyCode] + * from [link](https://api.moonpay.com/v3/currencies/) + */ +internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? + get() = when (this) { + Algorand -> MoonPaySupportedCurrency(networkCode = "algorand", currencyCode = "algo") + Aptos -> MoonPaySupportedCurrency(networkCode = "aptos", currencyCode = "apt") + Arbitrum -> MoonPaySupportedCurrency(networkCode = "arbitrum", currencyCode = "eth_arbitrum") + Avalanche -> MoonPaySupportedCurrency(networkCode = "avalanche_c_chain", currencyCode = "avax_cchain") + Binance -> MoonPaySupportedCurrency(networkCode = "bnb_chain", currencyCode = "bnb") + Bitcoin -> MoonPaySupportedCurrency(networkCode = "bitcoin", currencyCode = "btc") + BitcoinCash -> MoonPaySupportedCurrency(networkCode = "bitcoin_cash", currencyCode = "bch") + BSC -> MoonPaySupportedCurrency(networkCode = "binance_smart_chain", currencyCode = "bnb_bsc") + Cardano -> MoonPaySupportedCurrency(networkCode = "cardano", currencyCode = "ada") + Cosmos -> MoonPaySupportedCurrency(networkCode = "cosmos", currencyCode = "atom") + Dogecoin -> MoonPaySupportedCurrency(networkCode = "dogecoin", currencyCode = "doge") + Ethereum -> MoonPaySupportedCurrency(networkCode = "ethereum", currencyCode = "eth") + EthereumClassic -> MoonPaySupportedCurrency(networkCode = "ethereum_classic", currencyCode = "etc") + Hedera -> MoonPaySupportedCurrency(networkCode = "hedera", currencyCode = "hbar") + Litecoin -> MoonPaySupportedCurrency(networkCode = "litecoin", currencyCode = "ltc") + Near -> MoonPaySupportedCurrency(networkCode = "near", currencyCode = "near") + Optimism -> MoonPaySupportedCurrency(networkCode = "optimism", currencyCode = "eth_optimism") + Polkadot -> MoonPaySupportedCurrency(networkCode = "polkadot", currencyCode = "dot") + Polygon -> MoonPaySupportedCurrency(networkCode = "polygon", currencyCode = "matic_polygon") + Ravencoin -> MoonPaySupportedCurrency(networkCode = "ravencoin", currencyCode = "rvn") + Solana -> MoonPaySupportedCurrency(networkCode = "solana", currencyCode = "sol") + Stellar -> MoonPaySupportedCurrency(networkCode = "stellar", currencyCode = "xlm") + Tezos -> MoonPaySupportedCurrency(networkCode = "tezos", currencyCode = "xtz") + TON -> MoonPaySupportedCurrency(networkCode = "ton", currencyCode = "ton") + Tron -> MoonPaySupportedCurrency(networkCode = "tron", currencyCode = "trx") + VeChain -> MoonPaySupportedCurrency(networkCode = "vechain", currencyCode = "vet") + XRP -> MoonPaySupportedCurrency(networkCode = "ripple", currencyCode = "xrp") + Sei -> MoonPaySupportedCurrency(networkCode = "sei", currencyCode = "sei_sei") + ZkSyncEra -> MoonPaySupportedCurrency(networkCode = "zksync", currencyCode = "eth_zksync") + Base -> MoonPaySupportedCurrency(networkCode = "base", currencyCode = "eth_base") + Filecoin -> MoonPaySupportedCurrency(networkCode = "filecoin", currencyCode = "fil") + Sui -> MoonPaySupportedCurrency(networkCode = "sui", currencyCode = "sui") + Core -> MoonPaySupportedCurrency(networkCode = "core", currencyCode = "core") + ArbitrumTestnet -> null + AvalancheTestnet -> null + BinanceTestnet -> null + BSCTestnet -> null + BitcoinTestnet -> null + BitcoinCashTestnet -> null + CosmosTestnet -> null + Ducatus -> null + EthereumTestnet -> null + EthereumClassicTestnet -> null + Fantom -> null + FantomTestnet -> null + NearTestnet -> null + PolkadotTestnet -> null + Kava -> null // doesn't support KavaEvm + KavaTestnet -> null + Kusama -> null + PolygonTestnet -> null + RSK -> null + SeiTestnet -> null + StellarTestnet -> null + SolanaTestnet -> null + TronTestnet -> null + Gnosis -> null + Dash -> null + OptimismTestnet -> null + Dischain -> null + EthereumPow -> null + EthereumPowTestnet -> null + Kaspa -> null + Telos -> null + TelosTestnet -> null + TONTestnet -> null + RavencoinTestnet -> null + TerraV1 -> null + TerraV2 -> null + Cronos -> null + AlephZero -> null + AlephZeroTestnet -> null + OctaSpace -> null + OctaSpaceTestnet -> null + Chia -> null + ChiaTestnet -> null + Decimal -> null + DecimalTestnet -> null + XDC -> null + XDCTestnet -> null + VeChainTestnet -> null + AptosTestnet -> null + Playa3ull -> null + Shibarium -> null + ShibariumTestnet -> null + AlgorandTestnet -> null + HederaTestnet -> null + Aurora -> null + AuroraTestnet -> null + Areon -> null + AreonTestnet -> null + PulseChain -> null + PulseChainTestnet -> null + ZkSyncEraTestnet -> null + Nexa -> null + NexaTestnet -> null + Moonbeam -> null + MoonbeamTestnet -> null + Manta -> null + MantaTestnet -> null + PolygonZkEVM -> null + PolygonZkEVMTestnet -> null + Radiant -> null + BaseTestnet -> null + Moonriver -> null + MoonriverTestnet -> null + Mantle -> null + MantleTestnet -> null + Flare -> null + FlareTestnet -> null + Taraxa -> null + TaraxaTestnet -> null + Koinos -> null + KoinosTestnet -> null + Joystream -> null + Bittensor -> null + Blast -> null + BlastTestnet -> null + Cyber -> null + CyberTestnet -> null + InternetComputer -> null + SuiTestnet -> null + EnergyWebChain -> null + EnergyWebChainTestnet -> null + EnergyWebX -> null + EnergyWebXTestnet -> null + Casper -> null + CasperTestnet -> null + CoreTestnet -> null + Unknown -> null + Xodex -> null + Canxium -> null + } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index a2c9dd483f..f626aa4238 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -5,11 +5,11 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.redux.StateDialog import com.tangem.domain.wallets.models.UserWallet +import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState -import com.tangem.tap.domain.sdk.TangemSdkManager import com.tangem.tap.network.exchangeServices.ExchangeService import org.rekotlin.Action import org.rekotlin.Store diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt index 0f3c13fbf9..86116d6849 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt @@ -1,31 +1,15 @@ package com.tangem.tap.proxy.redux -import com.tangem.core.navigation.email.EmailSender import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository -import com.tangem.feature.qrscanning.QrScanningRouter -import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter -import com.tangem.features.send.api.navigation.SendRouter -import com.tangem.features.staking.api.navigation.StakingRouter -import com.tangem.features.tester.api.TesterRouter -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter -import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import org.rekotlin.Action sealed interface DaggerGraphAction : Action { data class SetActivityDependencies( - val testerRouter: TesterRouter, val scanCardUseCase: ScanCardUseCase, - val walletRouter: WalletRouter, val walletConnectInteractor: WalletConnectInteractor, - val tokenDetailsRouter: TokenDetailsRouter, val cardSdkConfigRepository: CardSdkConfigRepository, - val sendRouter: SendRouter, - val qrScanningRouter: QrScanningRouter, - val emailSender: EmailSender, - val stakingRouter: StakingRouter, - val pushNotificationsRouter: PushNotificationsRouter, ) : DaggerGraphAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt index 2a20bab5b6..76802dd20d 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt @@ -13,17 +13,9 @@ object DaggerGraphReducer { private fun internalReduce(action: DaggerGraphAction, state: AppState): DaggerGraphState { return when (action) { is DaggerGraphAction.SetActivityDependencies -> state.daggerGraphState.copy( - testerRouter = action.testerRouter, scanCardUseCase = action.scanCardUseCase, - walletRouter = action.walletRouter, walletConnectInteractor = action.walletConnectInteractor, - tokenDetailsRouter = action.tokenDetailsRouter, cardSdkConfigRepository = action.cardSdkConfigRepository, - sendRouter = action.sendRouter, - qrScanningRouter = action.qrScanningRouter, - emailSender = action.emailSender, - stakingRouter = action.stakingRouter, - pushNotificationsRouter = action.pushNotificationsRouter, ) } } diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 56c13b9a41..075686701f 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -1,14 +1,13 @@ package com.tangem.tap.proxy.redux -import com.tangem.TangemSdkLogger import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.blockchainsdk.signer.TransactionSignerFactory import com.tangem.common.routing.AppRouter -import com.tangem.core.navigation.email.EmailSender import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener -import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.connection.NetworkConnectionManager +import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository @@ -17,43 +16,32 @@ import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.feedback.GetCardInfoUseCase -import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.usercountry.GetUserCountryUseCase -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase -import com.tangem.feature.qrscanning.QrScanningRouter +import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles +import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter -import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter -import com.tangem.features.tester.api.TesterRouter -import com.tangem.features.tokendetails.navigation.TokenDetailsRouter -import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles import com.tangem.tap.proxy.AppStateHolder import org.rekotlin.StateType data class DaggerGraphState( - val testerRouter: TesterRouter? = null, val networkConnectionManager: NetworkConnectionManager? = null, val cardScanningFeatureToggles: CardScanningFeatureToggles? = null, val scanCardUseCase: ScanCardUseCase? = null, - val walletRouter: WalletRouter? = null, val walletConnectRepository: LegacyWalletConnectRepository? = null, - val walletConnectSessionsRepository: WalletConnectSessionsRepository? = null, val walletConnectInteractor: WalletConnectInteractor? = null, - val tokenDetailsRouter: TokenDetailsRouter? = null, val scanCardProcessor: ScanCardProcessor? = null, val cardSdkConfigRepository: CardSdkConfigRepository? = null, val appCurrencyRepository: AppCurrencyRepository? = null, @@ -62,23 +50,16 @@ data class DaggerGraphState( val appThemeModeRepository: AppThemeModeRepository? = null, val balanceHidingRepository: BalanceHidingRepository? = null, val walletsRepository: WalletsRepository? = null, - val networksRepository: NetworksRepository? = null, - val sendRouter: SendRouter? = null, - val qrScanningRouter: QrScanningRouter? = null, - val currenciesRepository: CurrenciesRepository? = null, val generalUserWalletsListManager: UserWalletsListManager? = null, val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null, val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null, val generateWalletNameUseCase: GenerateWalletNameUseCase? = null, val cardRepository: CardRepository? = null, - val tangemSdkLogger: TangemSdkLogger? = null, val settingsRepository: SettingsRepository? = null, val blockchainSDKFactory: BlockchainSDKFactory? = null, - val emailSender: EmailSender? = null, - val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase? = null, val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase? = null, val getCardInfoUseCase: GetCardInfoUseCase? = null, - val assetLoader: AssetLoader? = null, + val issuersConfigStorage: IssuersConfigStorage? = null, val stakingRouter: StakingRouter? = null, val urlOpener: UrlOpener? = null, val shareManager: ShareManager? = null, @@ -87,4 +68,7 @@ data class DaggerGraphState( val transactionSignerFactory: TransactionSignerFactory? = null, val homeFeatureToggles: HomeFeatureToggles? = null, val getUserCountryUseCase: GetUserCountryUseCase? = null, + val onrampFeatureToggles: OnrampFeatureToggles? = null, + val environmentConfigStorage: EnvironmentConfigStorage? = null, + val onboardingV2FeatureToggles: OnboardingV2FeatureToggles? = null, ) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 04e0e5f804..b031d03e46 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -11,6 +11,10 @@ import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.markets.details.MarketsTokenDetailsComponent +import com.tangem.features.onramp.component.BuyCryptoComponent +import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent +import com.tangem.features.onramp.component.OnrampComponent +import com.tangem.features.onramp.component.SellCryptoComponent import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter @@ -46,6 +50,10 @@ internal class ChildFactory @Inject constructor( private val disclaimerComponentFactory: DisclaimerComponent.Factory, private val manageTokensComponentFactory: ManageTokensComponent.Factory, private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory, + private val onrampComponentFactory: OnrampComponent.Factory, + private val buyCryptoComponentFactory: BuyCryptoComponent.Factory, + private val sellCryptoComponentFactory: SellCryptoComponent.Factory, + private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory, private val sendRouter: SendRouter, private val tokenDetailsRouter: TokenDetailsRouter, private val walletRouter: WalletRouter, @@ -186,6 +194,34 @@ internal class ChildFactory @Inject constructor( componentFactory = marketsTokenDetailsComponentFactory, ) } + is AppRoute.Onramp -> { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = OnrampComponent.Params(), + componentFactory = onrampComponentFactory, + ) + } + is AppRoute.BuyCrypto -> { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = BuyCryptoComponent.Params(userWalletId = route.userWalletId), + componentFactory = buyCryptoComponentFactory, + ) + } + is AppRoute.SellCrypto -> { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = SellCryptoComponent.Params(userWalletId = route.userWalletId), + componentFactory = sellCryptoComponentFactory, + ) + } + is AppRoute.Onboarding -> { + route.asComponentChild( + contextProvider = contextProvider(route, contextFactory), + params = OnboardingEntryComponent.Params(route.scanResponse), + componentFactory = onboardingEntryComponentFactory, + ) + } } } diff --git a/app/src/main/res/resources.properties b/app/src/main/res/resources.properties new file mode 100644 index 0000000000..d5a3ddc92a --- /dev/null +++ b/app/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=en-US \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index e71d217293..188c0a6ef6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat + plugins { alias(deps.plugins.kotlin.android) apply false alias(deps.plugins.kotlin.jvm) apply false @@ -20,6 +22,16 @@ interface Injected { val fs: FileSystemOperations } +// Test Logging +subprojects { + tasks.withType { + testLogging { + exceptionFormat = TestExceptionFormat.FULL + showStandardStreams = true + } + } +} + val assembleInternalQA by tasks.registering { group = "build" description = "Builds internal APK to 'build/outputs' directory" diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 398cb634a8..6744b6c0be 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -7,6 +7,7 @@ import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.navigation.Route import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrency @@ -117,10 +118,6 @@ sealed class AppRoute(val path: String) : Route { ) : AppRoute(path = "/details/security"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) - - companion object { - const val USER_WALLET_ID_KEY = "userWalletId" - } } @Serializable @@ -288,4 +285,20 @@ sealed class AppRoute(val path: String) : Route { val source: String, ) } + + @Serializable + data object Onramp : AppRoute(path = "/onramp") + + @Serializable + data class BuyCrypto( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/buy_crypto/${userWalletId.stringValue}") + + @Serializable + data class SellCrypto( + val userWalletId: UserWalletId, + ) : AppRoute(path = "/sell_crypto/${userWalletId.stringValue}") + + // Onboarding V2 + data class Onboarding(val scanResponse: ScanResponse) : AppRoute(path = "/onboarding_v2") } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt index 9597b3d764..4fbf0d991a 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt @@ -4,10 +4,15 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.amountScreen.utils.checkExceedBalance import com.tangem.common.ui.amountScreen.utils.getFiatValue import com.tangem.common.ui.amountScreen.utils.getKeyboardAction +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero @@ -22,8 +27,12 @@ import java.math.BigDecimal */ class AmountReduceByTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val minimumTransactionAmount: EnterAmountBoundary?, private val value: ReduceByData, ) : Transformer { + + private val maxEnterAmountConverter = MaxEnterAmountConverter() + override fun transform(prevState: AmountState): AmountState { if (prevState !is AmountState.Data) return prevState @@ -40,24 +49,41 @@ class AmountReduceByTransformer( decimals = fiatDecimals, ) + val maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatus) + val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue - val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) + val isExceedBalance = checkValue.checkExceedBalance(maxEnterAmount, amountTextField) + val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } ?: false val isZero = if (amountTextField.isFiatValue) { decimalFiatValue.isNullOrZero() } else { decimalCryptoValue.isNullOrZero() } + val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided return prevState.copy( - isPrimaryButtonEnabled = !isExceedBalance && !isZero, + isPrimaryButtonEnabled = !isZero && !isCheckFailed, amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, - isError = isExceedBalance, - error = resourceReference(R.string.send_validation_amount_exceeds_balance), + isError = isCheckFailed, + error = when { + isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance) + isLessThanMinimumIfProvided -> { + val minimumAmount = minimumTransactionAmount + ?.amount + ?.format { crypto(cryptoCurrencyStatus.currency) } + .orEmpty() + resourceReference( + R.string.transfer_notification_invalid_minimum_transaction_amount_text, + wrappedList(minimumAmount, minimumAmount), + ) + } + else -> TextReference.EMPTY + }, cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), keyboardOptions = KeyboardOptions( - imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue), + imeAction = getKeyboardAction(isCheckFailed, decimalCryptoValue), keyboardType = KeyboardType.Number, ), ), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt index c81634b818..7e5cf0ab8d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt @@ -4,11 +4,17 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.amountScreen.utils.checkExceedBalance import com.tangem.common.ui.amountScreen.utils.getFiatValue import com.tangem.common.ui.amountScreen.utils.getKeyboardAction +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.Transformer @@ -22,8 +28,11 @@ import java.math.BigDecimal */ class AmountReduceToTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val minimumTransactionAmount: EnterAmountBoundary?, private val value: BigDecimal, ) : Transformer { + private val maxEnterAmountConverter = MaxEnterAmountConverter() + override fun transform(prevState: AmountState): AmountState { if (prevState !is AmountState.Data) return prevState @@ -32,26 +41,44 @@ class AmountReduceToTransformer( val fiatDecimals = amountTextField.fiatAmount.decimals val cryptoValue = value.parseBigDecimal(cryptoDecimals) + val decimalCryptoValue = cryptoValue.parseToBigDecimal(cryptoDecimals) val (fiatValue, decimalFiatValue) = cryptoValue.getFiatValue( fiatRate = cryptoCurrencyStatus.value.fiatRate, isFiatValue = false, decimals = fiatDecimals, ) + val maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatus) + val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue - val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) + val isExceedBalance = checkValue.checkExceedBalance(maxEnterAmount, amountTextField) + val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } ?: false val isZero = if (amountTextField.isFiatValue) decimalFiatValue.isNullOrZero() else value.isNullOrZero() + val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided return prevState.copy( - isPrimaryButtonEnabled = !isExceedBalance && !isZero, + isPrimaryButtonEnabled = !isZero && !isCheckFailed, amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, - isError = isExceedBalance, - error = resourceReference(R.string.send_validation_amount_exceeds_balance), + isError = isCheckFailed, + error = when { + isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance) + isLessThanMinimumIfProvided -> { + val minimumAmount = minimumTransactionAmount + ?.amount + ?.format { crypto(cryptoCurrencyStatus.currency) } + .orEmpty() + resourceReference( + R.string.transfer_notification_invalid_minimum_transaction_amount_text, + wrappedList(minimumAmount, minimumAmount), + ) + } + else -> TextReference.EMPTY + }, cryptoAmount = amountTextField.cryptoAmount.copy(value = value), fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), keyboardOptions = KeyboardOptions( - imeAction = getKeyboardAction(isExceedBalance, value), + imeAction = getKeyboardAction(isCheckFailed, decimalCryptoValue), keyboardType = KeyboardType.Number, ), ), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index 410a23b468..f91822b7b7 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -3,17 +3,19 @@ package com.tangem.common.ui.amountScreen.converters import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverter -import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.AmountParameters import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.utils.BigDecimalFormatter.formatCryptoAmount -import com.tangem.core.ui.utils.BigDecimalFormatter.formatFiatAmount +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.wallets.models.UserWallet import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero @@ -24,17 +26,17 @@ import kotlinx.collections.immutable.persistentListOf * * @property clickIntents amount screen clicks * @property appCurrencyProvider selected app currency provider - * @property userWalletProvider selected user wallet provider + * @property maxEnterAmount max enter amount data * @property cryptoCurrencyStatusProvider current cryptocurrency status provider * @property iconStateConverter currency icon converter */ class AmountStateConverter( private val clickIntents: AmountScreenClickIntents, private val appCurrencyProvider: Provider, - private val userWalletProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + private val maxEnterAmount: EnterAmountBoundary, private val iconStateConverter: CryptoCurrencyToIconStateConverter, -) : Converter { +) : Converter { private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) { AmountFieldConverter( @@ -44,19 +46,18 @@ class AmountStateConverter( ) } - override fun convert(value: String): AmountState { - val userWallet = userWalletProvider() + override fun convert(value: AmountParameters): AmountState { val appCurrency = appCurrencyProvider() val status = cryptoCurrencyStatusProvider() - val fiat = formatFiatAmount(status.value.fiatAmount, appCurrency.code, appCurrency.symbol) - val crypto = formatCryptoAmount(status.value.amount, status.currency.symbol, status.currency.decimals) + val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) } + val crypto = maxEnterAmount.amount.format { crypto(status.currency) } val noFeeRate = status.value.fiatRate.isNullOrZero() return AmountState.Data( - walletName = userWallet.name, - walletBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)), + title = value.title, + availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)), tokenIconState = iconStateConverter.convert(status), - amountTextField = amountFieldConverter.convert(value), + amountTextField = amountFieldConverter.convert(value.value), isPrimaryButtonEnabled = false, appCurrencyCode = appCurrency.code, segmentedButtonConfig = persistentListOf( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/MaxEnterAmountConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/MaxEnterAmountConverter.kt new file mode 100644 index 0000000000..dce1957361 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/MaxEnterAmountConverter.kt @@ -0,0 +1,19 @@ +package com.tangem.common.ui.amountScreen.converters + +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.converter.Converter + +/** + * Converts [CryptoCurrencyStatus] to [EnterAmountBoundary] + */ +class MaxEnterAmountConverter : Converter { + + override fun convert(value: CryptoCurrencyStatus): EnterAmountBoundary { + return EnterAmountBoundary( + amount = value.value.amount, + fiatAmount = value.value.fiatAmount, + fiatRate = value.value.fiatRate, + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt index 8de99b1fc5..392fa817ba 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldChangeTransformer.kt @@ -5,12 +5,16 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.amountScreen.utils.checkExceedBalance import com.tangem.common.ui.amountScreen.utils.getCryptoValue import com.tangem.common.ui.amountScreen.utils.getFiatValue import com.tangem.common.ui.amountScreen.utils.getKeyboardAction import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.isNullOrZero @@ -20,11 +24,13 @@ import java.math.BigDecimal /** * Amount value change * - * @property cryptoCurrencyStatus current cryptocurrency status + * @property maxEnterAmount max amount to enter * @property value amount value */ class AmountFieldChangeTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val maxEnterAmount: EnterAmountBoundary, + private val minimumTransactionAmount: EnterAmountBoundary?, private val value: String, ) : Transformer { @@ -39,36 +45,50 @@ class AmountFieldChangeTransformer( val trimmedValue = value.trim() val cryptoValue = trimmedValue.getCryptoValue( - fiatRate = cryptoCurrencyStatus.value.fiatRate, + fiatRate = maxEnterAmount.fiatRate, isFiatValue = amountTextField.isFiatValue, decimals = cryptoDecimals, ) val decimalCryptoValue = cryptoValue.parseToBigDecimal(cryptoDecimals) val (fiatValue, decimalFiatValue) = trimmedValue.getFiatValue( - fiatRate = cryptoCurrencyStatus.value.fiatRate, + fiatRate = maxEnterAmount.fiatRate, isFiatValue = amountTextField.isFiatValue, decimals = fiatDecimals, ) val checkValue = if (amountTextField.isFiatValue) fiatValue else cryptoValue - val isExceedBalance = checkValue.checkExceedBalance(cryptoCurrencyStatus, amountTextField) + val isExceedBalance = checkValue.checkExceedBalance(maxEnterAmount, amountTextField) + val isLessThanMinimumIfProvided = minimumTransactionAmount?.amount?.let { decimalCryptoValue < it } ?: false val isZero = if (amountTextField.isFiatValue) { decimalFiatValue.isNullOrZero() } else { decimalCryptoValue.isNullOrZero() } + val isCheckFailed = isExceedBalance || isLessThanMinimumIfProvided return prevState.copy( - isPrimaryButtonEnabled = !isExceedBalance && !isZero, + isPrimaryButtonEnabled = !isZero && !isCheckFailed, amountTextField = amountTextField.copy( value = cryptoValue, fiatValue = fiatValue, - isError = isExceedBalance, - error = resourceReference(R.string.send_validation_amount_exceeds_balance).takeIf { isExceedBalance } - ?: TextReference.EMPTY, + isError = isCheckFailed, + error = when { + isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance) + isLessThanMinimumIfProvided -> { + val minimumAmount = minimumTransactionAmount + ?.amount + ?.format { crypto(cryptoCurrencyStatus.currency) } + .orEmpty() + resourceReference( + R.string.transfer_notification_invalid_minimum_transaction_amount_text, + wrappedList(minimumAmount, minimumAmount), + ) + } + else -> TextReference.EMPTY + }, cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), keyboardOptions = KeyboardOptions( - imeAction = getKeyboardAction(isExceedBalance, decimalCryptoValue), + imeAction = getKeyboardAction(isCheckFailed, decimalCryptoValue), keyboardType = KeyboardType.Number, ), ), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldMaxAmountTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldMaxAmountTransformer.kt deleted file mode 100644 index 05a8705750..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldMaxAmountTransformer.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.tangem.common.ui.amountScreen.converters.field - -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.AmountState -import com.tangem.core.ui.utils.parseBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.utils.isNullOrZero -import com.tangem.utils.transformer.Transformer -import java.math.RoundingMode - -/** - * Selects maximum amount value - * - * @property cryptoCurrencyStatus current cryptocurrency status - */ -class AmountFieldMaxAmountTransformer( - private val cryptoCurrencyStatus: CryptoCurrencyStatus, -) : Transformer { - - override fun transform(prevState: AmountState): AmountState { - if (prevState !is AmountState.Data) return prevState - - val amountTextField = prevState.amountTextField - - val cryptoDecimals = amountTextField.cryptoAmount.decimals - val fiatDecimals = amountTextField.fiatAmount.decimals - val decimalCryptoValue = cryptoCurrencyStatus.value.amount - val decimalFiatValue = cryptoCurrencyStatus.value.fiatAmount - - if (decimalCryptoValue.isNullOrZero()) return prevState - - val isDoneActionEnabled = !decimalCryptoValue.isNullOrZero() - val cryptoValue = decimalCryptoValue?.parseBigDecimal(cryptoDecimals).orEmpty() - val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals, roundingMode = RoundingMode.HALF_UP).orEmpty() - return prevState.copy( - isPrimaryButtonEnabled = true, - amountTextField = amountTextField.copy( - isValuePasted = true, - value = cryptoValue, - fiatValue = fiatValue, - isError = false, - cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), - fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), - keyboardOptions = KeyboardOptions( - imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, - keyboardType = KeyboardType.Number, - ), - ), - ) - } -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt new file mode 100644 index 0000000000..990e027cdf --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt @@ -0,0 +1,75 @@ +package com.tangem.common.ui.amountScreen.converters.field + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.KeyboardType +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.common.ui.amountScreen.utils.getKeyboardAction +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.utils.extensions.isZero +import com.tangem.utils.transformer.Transformer +import java.math.RoundingMode + +/** + * Selects maximum amount value + * + * @property maxAmount maximum enter amount + */ +class AmountFieldSetMaxAmountTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val maxAmount: EnterAmountBoundary, + private val minAmount: EnterAmountBoundary?, +) : Transformer { + + override fun transform(prevState: AmountState): AmountState { + if (prevState !is AmountState.Data) return prevState + + val amountTextField = prevState.amountTextField + + val cryptoDecimals = amountTextField.cryptoAmount.decimals + val fiatDecimals = amountTextField.fiatAmount.decimals + val decimalCryptoValue = maxAmount.amount + val decimalFiatValue = maxAmount.fiatAmount + + if (decimalCryptoValue == null || decimalCryptoValue.isZero()) return prevState + + val cryptoValue = decimalCryptoValue.parseBigDecimal(cryptoDecimals) + val fiatValue = decimalFiatValue?.parseBigDecimal(fiatDecimals, roundingMode = RoundingMode.HALF_UP).orEmpty() + val isLessThanMinimumIfProvided = minAmount?.amount?.let { decimalCryptoValue < it } ?: false + return prevState.copy( + isPrimaryButtonEnabled = !isLessThanMinimumIfProvided, + amountTextField = amountTextField.copy( + isValuePasted = true, + value = cryptoValue, + fiatValue = fiatValue, + isError = isLessThanMinimumIfProvided, + error = when { + isLessThanMinimumIfProvided -> { + val minimumAmount = minAmount + ?.amount + ?.format { crypto(cryptoCurrencyStatus.currency) } + .orEmpty() + resourceReference( + R.string.transfer_notification_invalid_minimum_transaction_amount_text, + wrappedList(minimumAmount, minimumAmount), + ) + } + else -> TextReference.EMPTY + }, + cryptoAmount = amountTextField.cryptoAmount.copy(value = decimalCryptoValue), + fiatAmount = amountTextField.fiatAmount.copy(value = decimalFiatValue), + keyboardOptions = KeyboardOptions( + imeAction = getKeyboardAction(isLessThanMinimumIfProvided, decimalCryptoValue), + keyboardType = KeyboardType.Number, + ), + ), + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountParameters.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountParameters.kt new file mode 100644 index 0000000000..e84bfb4330 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountParameters.kt @@ -0,0 +1,8 @@ +package com.tangem.common.ui.amountScreen.models + +import com.tangem.core.ui.extensions.TextReference + +data class AmountParameters( + val title: TextReference, + val value: String, +) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt index d23a2892bf..52758a02cc 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt @@ -13,8 +13,8 @@ sealed class AmountState { /** * @param isPrimaryButtonEnabled indicates if next state button enabled - * @param walletName user wallet name - * @param walletBalance user crypto currency balance in wallet + * @param title title + * @param availableBalance user crypto currency balance * @param tokenIconState crypto currency icon state * @param segmentedButtonConfig currency switcher config * @param selectedButton selected currency index @@ -24,8 +24,8 @@ sealed class AmountState { */ data class Data( override val isPrimaryButtonEnabled: Boolean, - val walletName: String, - val walletBalance: TextReference, + val title: TextReference, + val availableBalance: TextReference, val tokenIconState: CurrencyIconState, val segmentedButtonConfig: PersistentList, val selectedButton: Int, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/EnterAmountBoundary.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/EnterAmountBoundary.kt new file mode 100644 index 0000000000..bc21ab5104 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/EnterAmountBoundary.kt @@ -0,0 +1,22 @@ +package com.tangem.common.ui.amountScreen.models + +import java.math.BigDecimal + +data class EnterAmountBoundary( + val amount: BigDecimal? = null, + val fiatAmount: BigDecimal? = null, + val fiatRate: BigDecimal? = null, +) { + constructor( + amount: BigDecimal? = null, + fiatRate: BigDecimal? = null, + ) : this( + amount = amount, + fiatAmount = if (amount != null && fiatRate != null) { + amount * fiatRate + } else { + null + }, + fiatRate = fiatRate, + ) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt index a622f752c9..63c4d0cd5d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt @@ -17,8 +17,8 @@ object AmountStatePreviewData { val amountState = AmountState.Data( isPrimaryButtonEnabled = false, - walletName = "Family Wallet", - walletBalance = stringReference("2 130,88 USDT (2 129,92 \$)"), + title = stringReference("Family Wallet"), + availableBalance = stringReference("2 130,88 USDT (2 129,92 \$)"), tokenIconState = CurrencyIconState.Loading, segmentedButtonConfig = persistentListOf( AmountSegmentedButtonsConfig( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt index b2903b20cc..bcac8bb21b 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt @@ -19,16 +19,20 @@ import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.components.currency.icon.CurrencyIcon -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.format.bigdecimal.anyDecimals +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter +import java.math.BigDecimal @Composable fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDisabled: Boolean, onClick: () -> Unit) { if (amountState !is AmountState.Data) return val amount = amountState.amountTextField - val cryptoAmount = BigDecimalFormatter.formatWithSymbol(amount.value, amount.cryptoAmount.currencySymbol) + val cryptoAmount = formatWithSymbol(amount.value, amount.cryptoAmount.currencySymbol) val fiatAmount = BigDecimalFormatter.formatFiatAmount( fiatAmount = amount.fiatAmount.value, fiatCurrencySymbol = amount.fiatAmount.currencySymbol, @@ -77,6 +81,9 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis } } +fun formatWithSymbol(amount: String, symbol: String) = + BigDecimal.ZERO.format { crypto(symbol, 0).anyDecimals() }.replace("0", amount) + // region Preview @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt index d8347d67af..f2f8e078f9 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt @@ -24,6 +24,8 @@ import com.tangem.core.ui.components.fields.AmountTextField import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.rememberDecimalFormat @@ -92,11 +94,7 @@ private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: Stri ), ) { val text = if (amountField.isFiatValue) { - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = secondaryAmount.value, - cryptoCurrency = secondaryAmount.currencySymbol, - decimals = secondaryAmount.decimals, - ) + secondaryAmount.value.format { crypto(secondaryAmount.currencySymbol, secondaryAmount.decimals) } } else { BigDecimalFormatter.formatFiatAmount( fiatAmount = secondaryAmount.value, @@ -140,7 +138,7 @@ private fun AmountFieldError( exit = fadeOut(), modifier = modifier, ) { - val errorText = remember(this) { error } + val errorText = remember(this, error) { error } val color = if (isError) TangemTheme.colors.text.warning else TangemTheme.colors.text.attention Text( text = errorText.resolveReference(), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index caee01e48c..8b9904cc42 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -34,14 +34,14 @@ internal fun LazyListScope.amountField( .background(TangemTheme.colors.background.action), ) { Text( - text = amountState.walletName, + text = amountState.title.resolveReference(), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, modifier = Modifier .padding(top = TangemTheme.dimens.spacing14), ) - val balance = amountState.walletBalance.orMaskWithStars(isBalanceHidden).resolveReference() + val balance = amountState.availableBalance.orMaskWithStars(isBalanceHidden).resolveReference() AnimatedContent( targetState = balance, label = "Hide Balance Animation", diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt index 26492fa913..fe59dbf317 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/utils/AmountUtils.kt @@ -2,10 +2,10 @@ package com.tangem.common.ui.amountScreen.utils import androidx.compose.ui.text.input.ImeAction import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseToBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.utils.isNullOrZero +import com.tangem.utils.extensions.isZero import java.math.BigDecimal import java.math.RoundingMode @@ -37,11 +37,11 @@ internal fun String.getFiatValue( } internal fun String.checkExceedBalance( - cryptoCurrencyStatus: CryptoCurrencyStatus, + maxEnterAmount: EnterAmountBoundary, amountTextField: AmountFieldModel, ): Boolean { - val currencyCryptoAmount = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO - val currencyFiatAmount = cryptoCurrencyStatus.value.fiatAmount ?: BigDecimal.ZERO + val currencyCryptoAmount = maxEnterAmount.amount ?: BigDecimal.ZERO + val currencyFiatAmount = maxEnterAmount.fiatAmount ?: BigDecimal.ZERO val fiatDecimal = parseToBigDecimal(amountTextField.fiatAmount.decimals) val cryptoDecimal = parseToBigDecimal(amountTextField.cryptoAmount.decimals) return if (amountTextField.isFiatValue) { @@ -51,8 +51,8 @@ internal fun String.checkExceedBalance( } } -internal fun getKeyboardAction(isExceedBalance: Boolean, decimalCryptoValue: BigDecimal) = - if (!isExceedBalance && !decimalCryptoValue.isNullOrZero()) { +internal fun getKeyboardAction(isCheckFailed: Boolean, decimalCryptoValue: BigDecimal) = + if (!isCheckFailed && !decimalCryptoValue.isZero()) { ImeAction.Done } else { ImeAction.None diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index accc615136..858fdccb46 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -6,7 +6,9 @@ import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.shorted import java.math.BigDecimal sealed class NotificationUM(val config: NotificationConfig) { @@ -45,6 +47,14 @@ sealed class NotificationUM(val config: NotificationConfig) { ), ) + data class MinimumSendAmountError(val amount: String) : Error( + title = resourceReference(R.string.send_notification_invalid_amount_title), + subtitle = resourceReference( + R.string.transfer_notification_invalid_minimum_transaction_amount_text, + wrappedList(amount, amount), + ), + ) + data class TransactionLimitError( val cryptoCurrency: String, val utxoLimit: String, @@ -267,8 +277,8 @@ sealed class NotificationUM(val config: NotificationConfig) { subtitle = resourceReference( R.string.koinos_insufficient_mana_to_send_koin_description, formatArgs = wrappedList( - BigDecimalFormatter.formatCryptoAmountShorted(mana, "", Blockchain.Koinos.decimals()), - BigDecimalFormatter.formatCryptoAmountShorted(maxMana, "", Blockchain.Koinos.decimals()), + mana.format { crypto("", Blockchain.Koinos.decimals()).shorted() }, + maxMana.format { crypto("", Blockchain.Koinos.decimals()).shorted() }, ), ), ) @@ -286,11 +296,9 @@ sealed class NotificationUM(val config: NotificationConfig) { subtitle = resourceReference( R.string.koinos_mana_exceeds_koin_balance_description, formatArgs = wrappedList( - BigDecimalFormatter.formatCryptoAmount( - availableKoinForTransfer, - Blockchain.Koinos.currency, - Blockchain.Koinos.decimals(), - ), + availableKoinForTransfer.format { + crypto(Blockchain.Koinos.currency, Blockchain.Koinos.decimals()) + }, ), ), buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt index ade6bf5a42..13b0c42b88 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -6,7 +6,9 @@ import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.amountScreen.utils.getFiatString import com.tangem.core.ui.extensions.networkIconResId -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.uncapped import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency @@ -65,10 +67,23 @@ object NotificationsFactory { if (!isAccountFunded && reserveAmount != null && reserveAmount > sendingAmount) { add( NotificationUM.Error.ReserveAmount( - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = reserveAmount, - cryptoCurrency = cryptoCurrency, - ), + reserveAmount.format { + crypto(cryptoCurrency) + }, + ), + ) + } + } + + fun MutableList.addMinimumAmountErrorNotification( + minimumSendAmount: BigDecimal?, + sendingAmount: BigDecimal, + cryptoCurrency: CryptoCurrency, + ) { + if (minimumSendAmount != null && minimumSendAmount > sendingAmount) { + add( + NotificationUM.Error.MinimumSendAmountError( + amount = minimumSendAmount.format { crypto(cryptoCurrency) }, ), ) } @@ -87,10 +102,7 @@ object NotificationsFactory { NotificationUM.Error.TransactionLimitError( cryptoCurrency = cryptoCurrency.name, utxoLimit = utxoLimit.maxLimit.toPlainString(), - amountLimit = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = utxoLimit.maxAmount, - cryptoCurrency = cryptoCurrency, - ), + amountLimit = utxoLimit.maxAmount.format { crypto(cryptoCurrency) }, onConfirmClick = { onReduceClick( utxoLimit.maxAmount, @@ -124,10 +136,7 @@ object NotificationsFactory { if (existentialDeposit != null && diff >= BigDecimal.ZERO && existentialDeposit > diff) { add( NotificationUM.Error.ExistentialDeposit( - deposit = BigDecimalFormatter.formatCryptoAmountUncapped( - cryptoAmount = existentialDeposit, - cryptoCurrency = cryptoCurrency, - ), + deposit = existentialDeposit.format { crypto(cryptoCurrency).uncapped() }, onConfirmClick = { onReduceClick( existentialDeposit, @@ -155,10 +164,7 @@ object NotificationsFactory { if (isFeeCoverage) { add( NotificationUM.Warning.FeeCoverageNotification( - cryptoAmount = BigDecimalFormatter.formatCryptoAmountUncapped( - cryptoAmount = cryptoDiff, - cryptoCurrency = cryptoCurrency, - ), + cryptoAmount = cryptoDiff.format { crypto(cryptoCurrency).uncapped() }, fiatAmount = getFiatString( value = cryptoDiff, rate = fiatRate, @@ -187,7 +193,7 @@ object NotificationsFactory { if (isExceedsLimit) { add( NotificationUM.Error.MinimumAmountError( - amount = dustValue.parseBigDecimal(cryptoCurrencyStatus.currency.decimals), + amount = dustValue.format { crypto(cryptoCurrencyStatus.currency) }, ), ) } @@ -295,7 +301,7 @@ object NotificationsFactory { dustValue?.let { add( NotificationUM.Error.MinimumAmountError( - amount = it.parseBigDecimal(sendingCurrency.decimals), + amount = it.format { crypto(sendingCurrency) }, ), ) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index 964b1a0929..ae4993d6ce 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -1,9 +1,13 @@ package com.tangem.common.ui.tokens +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.YieldBalance @@ -25,16 +29,20 @@ import java.math.BigDecimal */ class TokenItemStateConverter( private val appCurrency: AppCurrency, + private val iconStateProvider: (CryptoCurrencyStatus) -> CurrencyIconState = { + CryptoCurrencyToIconStateConverter().convert(it) + }, private val titleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.TitleState = Companion::createTitleState, private val subtitleStateProvider: (CryptoCurrencyStatus) -> TokenItemState.SubtitleState? = { createSubtitleState(it, appCurrency) }, + private val fiatAmountStateProvider: (CryptoCurrencyStatus) -> TokenItemState.FiatAmountState? = { + createFiatAmountState(it, appCurrency) + }, private val onItemClick: (CryptoCurrencyStatus) -> Unit, private val onItemLongClick: ((CryptoCurrencyStatus) -> Unit)? = null, ) : Converter { - private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) - override fun convert(value: CryptoCurrencyStatus): TokenItemState { return when (value.value) { is CryptoCurrencyStatus.Loading -> value.mapToLoadingState() @@ -53,7 +61,7 @@ class TokenItemStateConverter( private fun CryptoCurrencyStatus.mapToLoadingState(): TokenItemState.Loading { return TokenItemState.Loading( id = currency.id.value, - iconState = iconStateConverter.convert(value = this), + iconState = iconStateProvider(this), titleState = titleStateProvider(this) as TokenItemState.TitleState.Content, subtitleState = requireNotNull(subtitleStateProvider(this)), ) @@ -62,13 +70,10 @@ class TokenItemStateConverter( private fun CryptoCurrencyStatus.mapToTokenItemState(): TokenItemState.Content { return TokenItemState.Content( id = currency.id.value, - iconState = iconStateConverter.convert(value = this), + iconState = iconStateProvider(this), titleState = titleStateProvider(this), subtitleState = requireNotNull(subtitleStateProvider(this)), - fiatAmountState = TokenItemState.FiatAmountState.Content( - text = getFormattedFiatAmount(), - hasStaked = !getStakedBalance().isZero(), - ), + fiatAmountState = requireNotNull(fiatAmountStateProvider(this)), subtitle2State = TokenItemState.Subtitle2State.TextContent(text = getFormattedAmount()), onItemClick = { onItemClick(this) }, onItemLongClick = onItemLongClick?.let { @@ -80,23 +85,13 @@ class TokenItemStateConverter( private fun CryptoCurrencyStatus.getFormattedAmount(): String { val amount = value.amount?.plus(getStakedBalance()) ?: return DASH_SIGN - return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) + return amount.format { crypto(currency) } } - private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { - val fiatYieldBalance = value.fiatRate?.times(getStakedBalance()).orZero() - val fiatAmount = value.fiatAmount?.plus(fiatYieldBalance) ?: return DASH_SIGN - - return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) - } - - private fun CryptoCurrencyStatus.getStakedBalance() = - (value.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance().orZero() - private fun CryptoCurrencyStatus.mapToUnreachableTokenItemState(): TokenItemState.Unreachable { return TokenItemState.Unreachable( id = currency.id.value, - iconState = iconStateConverter.convert(value = this), + iconState = iconStateProvider(this), titleState = titleStateProvider(this), subtitleState = subtitleStateProvider(this), onItemClick = { onItemClick(this) }, @@ -109,7 +104,7 @@ class TokenItemStateConverter( private fun CryptoCurrencyStatus.mapToNoAddressTokenItemState(): TokenItemState.NoAddress { return TokenItemState.NoAddress( id = currency.id.value, - iconState = iconStateConverter.convert(this), + iconState = iconStateProvider(this), titleState = titleStateProvider(this), subtitleState = subtitleStateProvider(this), onItemLongClick = onItemLongClick?.let { @@ -118,9 +113,9 @@ class TokenItemStateConverter( ) } - private companion object { + companion object { - fun createTitleState(currencyStatus: CryptoCurrencyStatus): TokenItemState.TitleState { + private fun createTitleState(currencyStatus: CryptoCurrencyStatus): TokenItemState.TitleState { return when (val value = currencyStatus.value) { is CryptoCurrencyStatus.Loading, is CryptoCurrencyStatus.MissedDerivation, @@ -142,7 +137,7 @@ class TokenItemStateConverter( } } - fun createSubtitleState( + private fun createSubtitleState( currencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency, ): TokenItemState.SubtitleState? { @@ -160,6 +155,29 @@ class TokenItemStateConverter( } } + private fun createFiatAmountState( + status: CryptoCurrencyStatus, + appCurrency: AppCurrency, + ): TokenItemState.FiatAmountState? { + return when (status.value) { + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> { + TokenItemState.FiatAmountState.Content( + text = status.getFormattedFiatAmount(appCurrency), + hasStaked = !status.getStakedBalance().isZero(), + ) + } + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Loading, + -> null + } + } + private fun CryptoCurrencyStatus.getCryptoPriceState(appCurrency: AppCurrency): TokenItemState.SubtitleState { val fiatRate = value.fiatRate val priceChange = value.priceChange @@ -167,10 +185,7 @@ class TokenItemStateConverter( return if (fiatRate != null && priceChange != null) { TokenItemState.SubtitleState.CryptoPriceContent( price = fiatRate.getFormattedCryptoPrice(appCurrency), - priceChangePercent = BigDecimalFormatter.formatPercent( - percent = priceChange, - useAbsoluteValue = true, - ), + priceChangePercent = priceChange.format { percent() }, type = priceChange.getPriceChangeType(), ) } else { @@ -189,5 +204,15 @@ class TokenItemStateConverter( private fun BigDecimal.getPriceChangeType(): PriceChangeType { return PriceChangeConverter.fromBigDecimal(value = this) } + + fun CryptoCurrencyStatus.getFormattedFiatAmount(appCurrency: AppCurrency): String { + val fiatYieldBalance = value.fiatRate?.times(getStakedBalance()).orZero() + val fiatAmount = value.fiatAmount?.plus(fiatYieldBalance) ?: return DASH_SIGN + + return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) + } + + private fun CryptoCurrencyStatus.getStakedBalance() = + (value.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance().orZero() } } \ No newline at end of file diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt index 3c1d48f13b..c623358a84 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt @@ -78,8 +78,6 @@ sealed class Basic( error = error, ) - class WalletOpened : Basic(event = "Wallet Opened") - class ButtonSupport(source: AnalyticsParam.ScreensSources) : Basic( event = "Request Support", params = mapOf( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt index bda2f0e17f..453d1e99c0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json +import java.math.BigDecimal data class ExchangeProvider( @Json(name = "id") @@ -26,6 +27,9 @@ data class ExchangeProvider( @Json(name = "recommended") val isRecommended: Boolean = false, + + @Json(name = "slippage") + val slippage: BigDecimal?, ) enum class ExchangeProviderType { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/MockedOnrampApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/MockedOnrampApi.kt new file mode 100644 index 0000000000..a960766de8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/MockedOnrampApi.kt @@ -0,0 +1,178 @@ +package com.tangem.datasource.api.onramp + +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.onramp.models.common.OnrampDestinationDTO +import com.tangem.datasource.api.onramp.models.request.OnrampPairsRequest +import com.tangem.datasource.api.onramp.models.response.OnrampDataResponse +import com.tangem.datasource.api.onramp.models.response.OnrampQuoteResponse +import com.tangem.datasource.api.onramp.models.response.OnrampStatusResponse +import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO +import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO +import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO +import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO + +internal class MockedOnrampApi : OnrampApi { + override suspend fun getCurrencies(): ApiResponse> = ApiResponse.Success( + COUNTRIES.map(OnrampCountryDTO::defaultCurrency), + ) + + override suspend fun getCountries(): ApiResponse> = ApiResponse.Success(COUNTRIES + RUSSIA) + + override suspend fun getCountryByIp(): ApiResponse = ApiResponse.Success(RUSSIA) + + override suspend fun getPaymentMethods(): ApiResponse> = ApiResponse.Success( + listOf( + PaymentMethodDTO(id = "google", name = "Google Play", image = ""), + PaymentMethodDTO(id = "apple", name = "Apple Pay", image = ""), + PaymentMethodDTO(id = "card", name = "Card", image = ""), + ), + ) + + override suspend fun getPairs(body: OnrampPairsRequest): ApiResponse> = ApiResponse.Success( + listOf( + OnrampPairDTO( + fromCurrencyCode = "USD", + to = OnrampDestinationDTO(contractAddress = "0xcontract_address", network = "ethereum"), + providers = listOf(), + ), + ), + ) + + override suspend fun getQuote( + fromCurrencyCode: String, + toContractAddress: String, + toNetwork: String, + paymentMethod: String, + countryCode: String, + fromAmount: String, + toDecimals: Int, + providerId: String, + ): ApiResponse { + TODO("Not yet implemented") + } + + override suspend fun getData( + fromCurrencyCode: String, + toContractAddress: String, + toNetwork: String, + paymentMethod: String, + countryCode: String, + fromAmount: String, + toDecimals: Int, + providerId: String, + toAddress: String, + redirectUrl: String, + language: String?, + theme: String?, + requestId: String, + ): ApiResponse { + TODO("Not yet implemented") + } + + override suspend fun getStatus(txId: String): ApiResponse { + TODO("Not yet implemented") + } + + private companion object { + private val RUSSIA = OnrampCountryDTO( + name = "Russia", + code = "RU", + image = "https://hatscripts.github.io/circle-flags/flags/ru.svg", + alpha3 = "RUS", + continent = "", + defaultCurrency = OnrampCurrencyDTO( + name = "Russian ruble", + code = "RUB", + image = "https://hatscripts.github.io/circle-flags/flags/ru.svg", + precision = 2, + ), + onrampAvailable = false, + ) + private val COUNTRIES = listOf( + OnrampCountryDTO( + name = "United States of America", + code = "USA", + image = "https://hatscripts.github.io/circle-flags/flags/us.svg", + alpha3 = "USA", + continent = "", + defaultCurrency = OnrampCurrencyDTO( + name = "US Dollar", + code = "USD", + image = "https://hatscripts.github.io/circle-flags/flags/us.svg", + precision = 2, + ), + onrampAvailable = true, + ), + OnrampCountryDTO( + name = "Europe Union", + code = "EU", + image = "https://hatscripts.github.io/circle-flags/flags/eu.svg", + alpha3 = "EUR", + continent = "", + defaultCurrency = OnrampCurrencyDTO( + name = "Euro", + code = "EUR", + image = "https://hatscripts.github.io/circle-flags/flags/eu.svg", + precision = 2, + ), + onrampAvailable = true, + ), + OnrampCountryDTO( + name = "Great Britain", + code = "GB", + image = "https://hatscripts.github.io/circle-flags/flags/gb.svg", + alpha3 = "GB", + continent = "", + defaultCurrency = OnrampCurrencyDTO( + name = "British Pound Sterling", + code = "GBP", + image = "https://hatscripts.github.io/circle-flags/flags/gb.svg", + precision = 2, + ), + onrampAvailable = true, + ), + OnrampCountryDTO( + name = "CANADA", + code = "CA", + image = "https://hatscripts.github.io/circle-flags/flags/ca.svg", + alpha3 = "CA", + continent = "", + defaultCurrency = OnrampCurrencyDTO( + name = "Canadian Dollar", + code = "CAD", + image = "https://hatscripts.github.io/circle-flags/flags/ca.svg", + precision = 2, + ), + onrampAvailable = true, + ), + OnrampCountryDTO( + name = "Hon Kong", + code = "HK", + image = "https://hatscripts.github.io/circle-flags/flags/hk.svg", + alpha3 = "HK", + continent = "", + defaultCurrency = OnrampCurrencyDTO( + name = "Hon Kong Dollar", + code = "HKD", + image = "https://hatscripts.github.io/circle-flags/flags/hk.svg", + precision = 2, + ), + onrampAvailable = true, + ), + OnrampCountryDTO( + name = "Australia", + code = "AU", + image = "https://hatscripts.github.io/circle-flags/flags/au.svg", + alpha3 = "AU", + continent = "", + defaultCurrency = OnrampCurrencyDTO( + name = "Australian Dollar", + code = "AUD", + image = "https://hatscripts.github.io/circle-flags/flags/au.svg", + precision = 2, + ), + onrampAvailable = true, + ), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt index 487f53ea0e..745fa5fe39 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/StakeKitApi.kt @@ -3,7 +3,8 @@ package com.tangem.datasource.api.stakekit import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.stakekit.models.request.* import com.tangem.datasource.api.stakekit.models.response.EnabledYieldsResponse -import com.tangem.datasource.api.stakekit.models.response.EnterActionResponse +import com.tangem.datasource.api.stakekit.models.response.ActionDTO +import com.tangem.datasource.api.stakekit.models.response.GetActionsResponse import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO @@ -35,14 +36,23 @@ interface StakeKitApi { @Body body: YieldBalanceRequestBody, ): ApiResponse> + @GET("actions") + suspend fun getActions( + @Query("walletAddress") walletAddress: String, + @Query("network") network: String, + @Query("status") status: String, + @Query("sort") sort: String = "createdAtDesc", + @Query("limit") limit: Int = 50, + ): ApiResponse + @POST("actions/enter") - suspend fun createEnterAction(@Body body: ActionRequestBody): ApiResponse + suspend fun createEnterAction(@Body body: ActionRequestBody): ApiResponse @POST("actions/exit") - suspend fun createExitAction(@Body body: ActionRequestBody): ApiResponse + suspend fun createExitAction(@Body body: ActionRequestBody): ApiResponse @POST("actions/pending") - suspend fun createPendingAction(@Body body: PendingActionRequestBody): ApiResponse + suspend fun createPendingAction(@Body body: PendingActionRequestBody): ApiResponse @POST("actions/enter/estimate-gas") suspend fun estimateGasOnEnter(@Body body: ActionRequestBody): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnterActionResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/ActionDTO.kt similarity index 97% rename from core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnterActionResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/ActionDTO.kt index 9413c43398..87382ee7ef 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/EnterActionResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/ActionDTO.kt @@ -9,7 +9,7 @@ import org.joda.time.DateTime import java.math.BigDecimal @JsonClass(generateAdapter = true) -data class EnterActionResponse( +data class ActionDTO( @Json(name = "id") val id: String, @Json(name = "integrationId") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/GetActionsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/GetActionsResponse.kt new file mode 100644 index 0000000000..0e116d42be --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/GetActionsResponse.kt @@ -0,0 +1,16 @@ +package com.tangem.datasource.api.stakekit.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GetActionsResponse( + @Json(name = "data") + val data: List, + @Json(name = "hasNextPage") + val hasNextPage: Boolean, + @Json(name = "limit") + val limit: Int, + @Json(name = "page") + val page: Int, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt index 330df3c666..88cc922696 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/stakekit/models/response/model/YieldDTO.kt @@ -7,33 +7,33 @@ import java.math.BigDecimal @JsonClass(generateAdapter = true) data class YieldDTO( @Json(name = "id") - val id: String, + val id: String?, @Json(name = "token") - val token: TokenDTO, + val token: TokenDTO?, @Json(name = "tokens") - val tokens: List, + val tokens: List?, @Json(name = "args") - val args: ArgsDTO, + val args: ArgsDTO?, @Json(name = "status") - val status: StatusDTO, + val status: StatusDTO?, @Json(name = "apy") - val apy: BigDecimal, + val apy: BigDecimal?, @Json(name = "rewardRate") - val rewardRate: Double, + val rewardRate: Double?, @Json(name = "rewardType") - val rewardType: RewardTypeDTO, + val rewardType: RewardTypeDTO?, @Json(name = "metadata") - val metadata: MetadataDTO, + val metadata: MetadataDTO?, @Json(name = "validators") - val validators: List, + val validators: List?, @Json(name = "isAvailable") - val isAvailable: Boolean, + val isAvailable: Boolean?, ) { @JsonClass(generateAdapter = true) data class StatusDTO( @Json(name = "enter") - val enter: Boolean, + val enter: Boolean?, @Json(name = "exit") val exit: Boolean?, ) @@ -41,21 +41,21 @@ data class YieldDTO( @JsonClass(generateAdapter = true) data class ArgsDTO( @Json(name = "enter") - val enter: Enter, + val enter: Enter?, @Json(name = "exit") val exit: Enter?, ) { @JsonClass(generateAdapter = true) data class Enter( @Json(name = "addresses") - val addresses: Addresses, + val addresses: Addresses?, @Json(name = "args") - val args: Map, + val args: Map?, ) { @JsonClass(generateAdapter = true) data class Addresses( @Json(name = "address") - val address: AddressArgumentDTO, + val address: AddressArgumentDTO?, @Json(name = "additionalAddresses") val additionalAddresses: Map? = null, ) @@ -65,11 +65,11 @@ data class YieldDTO( @JsonClass(generateAdapter = true) data class ValidatorDTO( @Json(name = "address") - val address: String, + val address: String?, @Json(name = "status") - val status: ValidatorStatusDTO, + val status: ValidatorStatusDTO?, @Json(name = "name") - val name: String, + val name: String?, @Json(name = "image") val image: String?, @Json(name = "website") @@ -83,7 +83,7 @@ data class YieldDTO( @Json(name = "votingPower") val votingPower: Double?, @Json(name = "preferred") - val preferred: Boolean, + val preferred: Boolean?, ) { @JsonClass(generateAdapter = true) enum class ValidatorStatusDTO { @@ -106,51 +106,51 @@ data class YieldDTO( @JsonClass(generateAdapter = true) data class MetadataDTO( @Json(name = "name") - val name: String, + val name: String?, @Json(name = "logoURI") - val logoUri: String, + val logoUri: String?, @Json(name = "description") - val description: String, + val description: String?, @Json(name = "documentation") val documentation: String?, @Json(name = "gasFeeToken") - val gasFeeTokenDTO: TokenDTO, + val gasFeeTokenDTO: TokenDTO?, @Json(name = "token") - val tokenDTO: TokenDTO, + val tokenDTO: TokenDTO?, @Json(name = "tokens") - val tokensDTO: List, + val tokensDTO: List?, @Json(name = "type") - val type: String, + val type: String?, @Json(name = "rewardSchedule") - val rewardSchedule: RewardScheduleDTO, + val rewardSchedule: RewardScheduleDTO?, @Json(name = "cooldownPeriod") val cooldownPeriod: PeriodDTO?, @Json(name = "warmupPeriod") - val warmupPeriod: PeriodDTO, + val warmupPeriod: PeriodDTO?, @Json(name = "rewardClaiming") - val rewardClaiming: RewardClaimingDTO, + val rewardClaiming: RewardClaimingDTO?, @Json(name = "defaultValidator") val defaultValidator: String?, @Json(name = "minimumStake") val minimumStake: Int?, @Json(name = "supportsMultipleValidators") - val supportsMultipleValidators: Boolean, + val supportsMultipleValidators: Boolean?, @Json(name = "revshare") - val revshare: EnabledDTO, + val revshare: EnabledDTO?, @Json(name = "fee") - val fee: EnabledDTO, + val fee: EnabledDTO?, ) { @JsonClass(generateAdapter = true) data class PeriodDTO( @Json(name = "days") - val days: Int, + val days: Int?, ) @JsonClass(generateAdapter = true) data class EnabledDTO( @Json(name = "enabled") - val enabled: Boolean, + val enabled: Boolean?, ) enum class RewardScheduleDTO { diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 8b38ca983a..720e14ac0b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -11,6 +11,7 @@ import com.tangem.datasource.api.common.config.managers.ProdApiConfigsManager import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.markets.TangemTechMarketsApi +import com.tangem.datasource.api.onramp.MockedOnrampApi import com.tangem.datasource.api.onramp.OnrampApi import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.tangemTech.TangemTechApi @@ -98,22 +99,24 @@ internal object NetworkModule { @Provides @Singleton fun provideOnrampApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - apiConfigsManager: ApiConfigsManager, - appLogsStore: AppLogsStore, + // @NetworkMoshi moshi: Moshi, + // @ApplicationContext context: Context, + // apiConfigsManager: ApiConfigsManager, + // appLogsStore: AppLogsStore, ): OnrampApi { - return createApi( - id = ApiConfig.ID.Express, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - clientBuilder = { - addInterceptor( - NetworkLogsSaveInterceptor(appLogsStore), - ) - }, - ) + // TODO: Remove when backend will be ready - [REDACTED_TASK_KEY] + return MockedOnrampApi() + // return createApi( + // id = ApiConfig.ID.Express, + // moshi = moshi, + // context = context, + // apiConfigsManager = apiConfigsManager, + // clientBuilder = { + // addInterceptor( + // NetworkLogsSaveInterceptor(appLogsStore), + // ) + // }, + // ) } @Provides diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingBalanceStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt similarity index 54% rename from core/datasource/src/main/java/com/tangem/datasource/di/StakingBalanceStoreModule.kt rename to core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt index 21d78318c1..3dda78cc57 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StakingBalanceStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt @@ -1,8 +1,9 @@ package com.tangem.datasource.di import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.token.* import com.tangem.datasource.local.token.DefaultStakingBalanceStore -import com.tangem.datasource.local.token.StakingBalanceStore +import com.tangem.datasource.local.token.DefaultStakingYieldsStore import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -11,11 +12,23 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object StakingBalanceStoreModule { +internal object StakingStoreModule { + + @Provides + @Singleton + fun provideStakingTokensStore(): StakingYieldsStore { + return DefaultStakingYieldsStore() + } @Provides @Singleton fun provideStakingBalanceStore(): StakingBalanceStore { return DefaultStakingBalanceStore(dataStore = RuntimeDataStore()) } + + @Provides + @Singleton + fun provideStakingActionsStore(): StakingActionsStore { + return DefaultStakingActionsStore(dataStore = RuntimeDataStore()) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt deleted file mode 100644 index d55c4066ec..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StakingTokensStoreModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.datasource.di - -import com.tangem.datasource.local.token.DefaultStakingYieldsStore -import com.tangem.datasource.local.token.StakingYieldsStore -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 StakingTokensStoreModule { - - @Provides - @Singleton - fun provideStakingTokensStore(): StakingYieldsStore { - return DefaultStakingYieldsStore() - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt index 109e138c0d..0ed4457201 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt @@ -4,6 +4,8 @@ import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.local.config.environment.DefaultEnvironmentConfigStorage import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.issuers.DefaultIssuersConfigStorage +import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage import com.tangem.datasource.local.config.providers.DefaultBlockchainProvidersStorage import com.tangem.datasource.local.config.testnet.DefaultTestnetTokensStorage @@ -30,7 +32,7 @@ internal object ConfigModule { @Provides @Singleton - fun providesTestnetTokensStorage(assetLoader: AssetLoader): TestnetTokensStorage { + fun provideTestnetTokensStorage(assetLoader: AssetLoader): TestnetTokensStorage { return DefaultTestnetTokensStorage(assetLoader) } @@ -42,4 +44,13 @@ internal object ConfigModule { runtimeStateStore = RuntimeStateStore(defaultValue = emptyMap()), ) } + + @Provides + @Singleton + fun provideIssuersConfigStorage(assetLoader: AssetLoader): IssuersConfigStorage { + return DefaultIssuersConfigStorage( + assetLoader = assetLoader, + runtimeStateStore = RuntimeStateStore(defaultValue = emptyList()), + ) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/DefaultIssuersConfigStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/DefaultIssuersConfigStorage.kt new file mode 100644 index 0000000000..0d9de3ea5c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/DefaultIssuersConfigStorage.kt @@ -0,0 +1,27 @@ +package com.tangem.datasource.local.config.issuers + +import com.tangem.datasource.asset.loader.AssetLoader +import com.tangem.datasource.local.config.issuers.models.Issuer +import com.tangem.datasource.local.datastore.RuntimeStateStore + +internal class DefaultIssuersConfigStorage( + private val assetLoader: AssetLoader, + private val runtimeStateStore: RuntimeStateStore>, +) : IssuersConfigStorage { + + override suspend fun getConfig(): List { + val cachedData = runtimeStateStore.get().value + + if (cachedData.isNotEmpty()) return cachedData + + val issuers = assetLoader.loadList(fileName = ISSUERS_FILE_NAME) + + runtimeStateStore.store(value = issuers) + + return issuers + } + + private companion object { + const val ISSUERS_FILE_NAME = "tangem-app-config/issuers" + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/IssuersConfigStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/IssuersConfigStorage.kt new file mode 100644 index 0000000000..ea2ac0708c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/IssuersConfigStorage.kt @@ -0,0 +1,14 @@ +package com.tangem.datasource.local.config.issuers + +import com.tangem.datasource.local.config.issuers.models.Issuer + +/** + * Storage for list of Twins [Issuer] + * +[REDACTED_AUTHOR] + */ +interface IssuersConfigStorage { + + /** Get config */ + suspend fun getConfig(): List +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/models/Issuer.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/models/Issuer.kt new file mode 100644 index 0000000000..a26754c4ec --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/issuers/models/Issuer.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.local.config.issuers.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Issuer( + @Json(name = "privateKey") val privateKey: String, + @Json(name = "publicKey") val publicKey: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt index 88b241c3ff..e168276a5b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt @@ -5,6 +5,7 @@ import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.extensions.addOrReplace +import com.tangem.utils.extensions.replaceBy import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -27,4 +28,23 @@ internal class DefaultNetworksStatusesStore( store(key, newValues) } } + + override suspend fun storeAll(key: UserWalletId, values: Collection) { + mutex.withLock { + val currentValues = getSyncOrNull(key) ?: emptySet() + val updatedValues = currentValues.toMutableSet() + + values.forEach { newValue -> + val isReplaced = updatedValues.replaceBy(newValue) { + it.network == newValue.network + } + + if (!isReplaced) { + updatedValues.add(newValue) + } + } + + store(key, updatedValues) + } + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/NetworksStatusesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/NetworksStatusesStore.kt index a30a1b49ab..3ab684275f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/NetworksStatusesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/NetworksStatusesStore.kt @@ -11,4 +11,6 @@ interface NetworksStatusesStore { suspend fun getSyncOrNull(key: UserWalletId): Set? suspend fun store(key: UserWalletId, value: NetworkStatus) + + suspend fun storeAll(key: UserWalletId, values: Collection) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingActionsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingActionsStore.kt new file mode 100644 index 0000000000..39f499ca68 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingActionsStore.kt @@ -0,0 +1,34 @@ +package com.tangem.datasource.local.token + +import com.tangem.datasource.local.datastore.core.StringKeyDataStore +import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +internal class DefaultStakingActionsStore( + private val dataStore: StringKeyDataStore>, +) : StakingActionsStore { + + private val mutex = Mutex() + + override fun get(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow> { + return dataStore.get(composeKey(userWalletId, cryptoCurrencyId)) + } + + override suspend fun store( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + items: List, + ) { + mutex.withLock { + dataStore.store(composeKey(userWalletId, cryptoCurrencyId), items) + } + } + + private fun composeKey(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): String { + return userWalletId.stringValue + cryptoCurrencyId.value + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt index db8de27d38..d30a83e4c3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt @@ -4,13 +4,13 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO internal class DefaultStakingYieldsStore : StakingYieldsStore { - private var yields = mutableListOf() + private var yields = listOf() override fun get(): List { return yields } override fun store(items: List) { - yields = items.toMutableList() + yields = items } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingActionsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingActionsStore.kt new file mode 100644 index 0000000000..aa6de02da4 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingActionsStore.kt @@ -0,0 +1,13 @@ +package com.tangem.datasource.local.token + +import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +interface StakingActionsStore { + + fun get(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow> + + suspend fun store(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, items: List) +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/inner/InnerNavigation.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/inner/InnerNavigation.kt new file mode 100644 index 0000000000..eecc01d57f --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/inner/InnerNavigation.kt @@ -0,0 +1,16 @@ +package com.tangem.core.decompose.navigation.inner + +import kotlinx.coroutines.flow.StateFlow + +/** + * Interface to provide internal navigation access from child to parent + */ +interface InnerNavigation { + val state: StateFlow + fun pop(onComplete: (Boolean) -> Unit) +} + +interface InnerNavigationState { + val stackSize: Int + val stackMaxSize: Int? +} \ No newline at end of file diff --git a/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/inner/InnerNavigationHolder.kt b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/inner/InnerNavigationHolder.kt new file mode 100644 index 0000000000..3c60bfb7b7 --- /dev/null +++ b/core/decompose/src/main/kotlin/com/tangem/core/decompose/navigation/inner/InnerNavigationHolder.kt @@ -0,0 +1,5 @@ +package com.tangem.core.decompose.navigation.inner + +interface InnerNavigationHolder { + val innerNavigation: InnerNavigation +} \ No newline at end of file diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json index 0aa128c712..2751047561 100644 --- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json @@ -5,16 +5,12 @@ }, { "name": "WC_SOLANA_TX_SIGN_ENABLED", - "version": "undefined" + "version": "5.18.0" }, { "name": "STAKING_ENABLED", "version": "5.15.0" }, - { - "name": "MARKETS_ENABLED", - "version": "5.15.0" - }, { "name": "IS_ETHEREUM_EIP_1559_ENABLED", "version": "5.17.0" @@ -22,5 +18,17 @@ { "name": "MIGRATE_USER_COUNTRY_CODE_ENABLED", "version": "5.17.0" + }, + { + "name": "ONRAMP_ENABLED", + "version": "undefined" + }, + { + "name": "MAIN_ACTION_BUTTONS_ENABLED", + "version": "undefined" + }, + { + "name": "ONBOARDING_CODE_REFACTORING_ENABLED", + "version": "undefined" } ] diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 745138a008..db671f2128 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1,5 +1,11 @@ + Token nicht in Deinem Portfolio gefunden? Überprüfe die Märkte, um es zu finden und zum Kauf hinzuzufügen + Token nicht in Deinem Portfolio gefunden? Überprüfe die Märkte, um es zu finden und zum Verkauf hinzuzufügen + Wähle den Token + Token nicht in Deinem Portfolio gefunden? Überprüfe die Märkte, um es für Swaps zu finden und hinzuzufügen + Es gibt keine verfügbaren Token, die mit dem ausgewählten Token getauscht werden können. Bitte wähle einen anderen. + Kein verfügbares Paar Netzwerk wählen Token anlegen Token verwalten @@ -551,9 +557,22 @@ Zugangscode wiederherstellen Identische Karten oder Ring Zugangscode + Suche nach Land + Nicht verfügbar + Suche nach Währung + Der Kaufbetrag sollte nicht höher sein als %s + Der zu kaufende Betrag muss mindestens %s betragen + Keine verfügbaren Anbieter für diese Währung + Bezahlen mit + Du kannst Deine Transaktion beim Drittanbieter %s abschließen. + Umleitung auf %s... Unsere Dienstleistungen sind in diesem Land nicht verfügbar Änder oder bestätige bitte Dein Wohnsitz wurde identifiziert als + Residenz + Bitte wähle das richtige Land aus, um korrekte Zahlungsoptionen und Dienstleistungen zu gewährleisten. + Einstellungen + Über Gruppe erstellen Nach Guthaben Token organisieren @@ -696,6 +715,7 @@ Name Die Anzahl der zu stakenden Krypros muss mindesten %s betragen Der Stakingbetrag wird aufgrund der Netzwerkregeln auf %1$s TRX aufgerundet. + Der Betrag der unstaked wird, wird aufgrund der Netzwerkregeln auf %1$s TRX gerundet. Nicht gestakte beanspruche Gebühr für das Staking-Konto Ein Staking-Konto ist ein spezielles Konto, auf dem eingesetzte SOL-Token gelagert werden. Es wird erstellt, wenn du deine Token an einen Validator delegierst, um an der Transaktionsvalidierung teilzunehmen und Belohnungen zu verdienen. Für die Erstellung des Staking-Kontos wird eine geringe Gebühr erhoben, die nach Abschluss des Stakings zurückgegeben wird. @@ -738,10 +758,13 @@ Mit dem Staking kannst Du %1$s verdienen. Deine Staking-Belohnungen erhältst Du jede Woche. Sicher staken und wöchentliche Belohnungen verdienen. Verdiene Staking-Belohnungen + Dein verbleibender Stakingbetrag ist zu niedrig, um den Einsatz aufzuheben. Du musst mehr Staken, um den Mindestbetrag zum Aufheben des Einsatzes zu erreichen. + Niedriges Stakingguthaben Aufgrund von Netzwerkproblemen ist Staking derzeit nicht verfügbar. Bitte versuche es später erneut. Beim Staking im %1$s -Netzwerk mit einem neuen Validator werden alle zuvor eingesetzten Kryptos automatisch an diesen Validator übertragen Reinvestiert Deine verdienten Prämien in Deinen Einsatzbetrag und erhöht so den potenziellen Gewinn. Mit Restake kannst Du Dein Guthaben von einem Validator zu einem anderen verschieben, ohne dass Du den Stake aufheben musst. + Du bist dabei, Dein gesamtes Guthaben zu staken. Wir empfehlen, einen kleinen Betrag übrig zu lassen, um die Netzwerkgebühren für die Aufhebung des Stakes oder das Einfordern von Prämien abzudecken. Entsperre dein Geld, um es aus dem Staking-Prozess abzuheben. Das Freischalten nimmt %s. Nach Ablauf der 21-tägigen Bindungsfrist kannst Du über Dein Guthaben verfügen. Die Prämie wird zusammen mit dem ungestaketen Guthaben abgehoben. Deine Assets steht Dir nach Ablauf der Frist für die Aufhebung der Bindung %s zur Verfügung. @@ -770,6 +793,7 @@ Belohnungen Stake gesperrt Mehr staken + Gestakeder Betrag Du setzt %1$s ein und ethältst Deine Belohnung %2$s Zum Entsperren antippen Tippe zum Entsperren oder Abstimmen @@ -780,6 +804,7 @@ Lösen der Bindungen Gelocktes unlocken Entsperren + Der unstaking-Betrag muss mindestens %s betragen. Der Betrag übersteigt das eingesetzte Guthaben Unstaken Staking beenden @@ -806,8 +831,10 @@ Tausche mehr Token zu besseren Kursen direkt in deiner Brieftasche. Neuer Swap-Anbieter verfügbar! Der Betrag umfasst:\n- Gebühr des Dienstanbieters\n- Netzgebühr für die Rücksendung von %s von der Vermittlungsstelle an die Adresse des Nutzers. + Der Betrag beinhaltet:\n• Honorar des Dienstleisters\n• Netzwerkgebühr für das Senden %1$s von der Börse zurück an die Adresse des Benutzers. \n\n Provider-Slippage kann bis zu %2$s Der Betrag enthält die Gebühren des Dienstleisters. - Gebühren + Der Betrag beinhaltet die Gebühr des Dienstleisters. \n\nProvider-Slippage kann bis zu %s + Information Alle dezentralen Börsen benötigen Genehmigungen, um zu verhindern, dass intelligente Verträge ohne Ihre Erlaubnis auf Ihre Geldbörse zugreifen. Smart Contracts können nicht auf Ihre Token zugreifen, wenn Sie nicht zustimmen. Indem Sie Ihre Token \"freischalten\", ermächtigen Sie den 1-Zoll-Smart-Contract, sie auszugeben. Die Miner des Netzwerks erhalten eine (von Ihnen bezahlte) Gasgebühr, um diese Aktion in der Blockchain aufzuzeichnen. Sie können Ihre Token tauschen, nachdem Sie Ihre Zustimmung gegeben haben. Genehmigen Fehler bei der Gebührenschätzung. Bitte sende dein Feedback an den Support. @@ -845,6 +872,9 @@ Tausche diesen Token gegen einen anderen zu %1$s Servicegebühren von Februar %2$s-%3$s. Tausche mit Changelly, %s Gebühren Jetzt tauschen + Nicht verfügbar zum Kauf + Nicht zum Verkauf verfügbar + Nicht verfügbar für Tausch von %s Vertrag: %s Du hast noch keine Transaktionen Der Transaktionsverlauf konnte nicht geladen werden.\nKlicke auf die Schaltfläche Neu laden, um die Informationen zu aktualisieren. @@ -854,6 +884,7 @@ von: %s zu: %s Validierer: %s + Der Mindesttransaktionsbetrag beträgt %1$s. Versuche es erneut Du hast dieselbe Karte oder Ring gescannt. Um ein Zwillings-Wallet zu erstellen, musst du die Karte oder Ring mit der Nummer %d scannen. Du hast die falsche Doppelkarte oder Ring gescannt. Bitte versuche eine andere Karte oder Ring @@ -989,7 +1020,7 @@ Das Solana-Netz ist überlastet. Wenn deine Transaktion nicht innerhalb von 2 Minuten bearbeitet wird, wiederhole bitte die Transaktion. Solana Netzwerkalarm Das Solana-Netzwerk erhebt alle 2 Tage eine Miete von %1$s. Konten, die sich die Miete nicht leisten können, werden aus dem Netzwerk gelöscht. Hinterlege deinem Konto mit mehr als %2$s, um es kostenlos zu nutzen. - Einige Netzwerke sind derzeit nicht erreichbar. Bitte versuche es später erneut. + Wischen Sie nach unten, um zu aktualisieren, oder versuchen Sie es später erneut. Einige Netzwerke sind nicht erreichbar Dies ist eine Testnet-Karte. Sie kann keine Transaktionen verarbeiten und sollte nur zu Test- und Entwicklungszwecken verwendet werden. Nur für Testzwecke diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 6703130663..8c35a676fa 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1,5 +1,11 @@ + ¿No encuentra el token en su billetera? Consulte los mercados para encontrarlo y añadirlo a la compra + ¿No encuentra el token en su billetera? Consulte los mercados para encontrarlo y añadirlo a la venta + Elige el token + ¿No encuentras el token en tu billetera? Consulte los mercados para encontrarlo y agregarlo al intercambio + No hay tokens disponibles para intercambiar con el token seleccionado. Por favor elige otro. + No hay par disponible Elige red Agregue un token personalizado Gestionar tokens @@ -88,7 +94,7 @@ Reclamar Reclame recompensas Cerrar - confirme + Confirme Continuar Copiar Copiar la dirección @@ -551,9 +557,20 @@ Restaurar código de acceso Tarjetas idénticas Código de acceso + Buscar por país + Indisponible + Buscar por moneda + El monto de la compra no debe ser mayor a %s + La cantidad a comprar debe ser como mínimo %s + No hay proveedores disponibles para esta moneda + Pagar con Nuestros servicios no están disponibles en este país Cámbielo o confírmelo Su residencia ha sido identificada como + Residencia + Seleccione el país correcto para garantizar opciones de pago y servicios precisos. + Ajustes + Vía Agrupar Por saldo Organizar tokens @@ -696,6 +713,7 @@ Nombre El monto del staking debe ser al menos %s El monto del staking se redondeará a %1$s TRX debido a las reglas de la red. + El monto de cancelación del staking se redondeará a %1$s TRX debido a las reglas de la red. Unstaking de la reclamación Tarifa de staking de la cuenta Una cuenta de staking es una cuenta especial donde se almacenan los tokens SOL de staking. Se crea cuando delegas sus tokens a un validador para participar en la validación de transacciones y ganar recompensas. Se cobra una pequeña tarifa por crear la cuenta de staking, que se devuelve una vez que se completa el staking. @@ -738,10 +756,13 @@ El staking le permite ganar %1$s. Sus recompensas por apostar llegan todas las semanas. Haga staking de forma segura y comience a ganar recompensas semanales Gane recompensas por staking + Su saldo de staking será demasiado bajo para cancelar el staking. Necesitará hacer staking más para alcanzar la cantidad mínima para cancelarlo. + Saldo de staking bajo El staking no está disponible actualmente debido a las condiciones de la red. Inténtelo de nuevo más tarde. El staking en la red %1$s con un nuevo validador transferirá automáticamente todos los fondos previamente en staking a este validador. Reinvierta las recompensas obtenidas en el monto apostado, aumentando las ganancias potenciales. La opción de volver a staking le permite mover sus fondos de un validador a otro sin necesidad de retirarlos. + Está a punto de hacer staking con todo su saldo. Recomendamos dejar una cantidad para cubrir las tarifas de la red por unstaking o reclamo de recompensas. Desbloquee su dinero para retirarlo del proceso de staking. El desbloqueo demora %s minuto. Sus fondos estarán disponibles para su uso después del período de desvinculación de 21 días. La recompensa se retirará junto con los fondos de desvinculación. Sus fondos estarán disponibles para su uso después del período de desvinculación de %s. @@ -770,6 +791,7 @@ Recompensas El stake está bloqueado Hacer más staking + Monto del staking Ud hace el staking de %1$s y recibirá %2$s Toque para desbloquear Pulse para desbloquear o votar @@ -780,6 +802,7 @@ Desunión Desbloquear Desbloqueando + El monto del staking debe ser al menos %s El monto excede el saldo apostado Sin staking Unstaking @@ -806,7 +829,9 @@ Intercambie más tokens a mejores tasas directamente en su billetera. ¡Nuevo proveedor de intercambio disponible! El monto incluye:\n• Tarifas del proveedor de servicios\n• Tarifas de red por enviar %s desde el intercambio a la dirección del usuario. + El monto incluye:\n• tarifas del proveedor de servicios\n• tarifas de red por enviar %1$s desde el intercambio a la dirección del usuario. \n\nEl slippage del proveedor puede alcanzar el %2$s El importe incluye los honorarios del proveedor de servicios. + El importe incluye las tarifas del proveedor de servicios. \n\nEl slippage del proveedor puede alcanzar el %s Tarifa Todos los exchanges descentralizados requieren aprobaciones para evitar que los smart contracts accedan a su billetera sin su permiso. Por diseño, los smart contracts no pueden acceder a tus tokens a menos que lo apruebes. Al \"desbloquear\" sus tokens, autoriza al smart contract de 1-inch a gastarlos. Los mineros de la red reciben una tarifa de gas (pagada por voz) para registrar esta acción en la blockchain. Puede intercambiar su token después de dar la aprobación. Aprobar @@ -845,6 +870,9 @@ Cambie este token por otro por una tarifa de servicio de %1$s del %2$s al %3$s de febrero. Intercambio con Changelly, %s tarifa Intercambie ahora + No disponible para compra + No disponible para vender + No disponible para cambio desde %s contacto: %s Aún no tiene ninguna transacción Error al cargar el historial de transacciones.\nHaga clic en el botón de recarga para actualizar la información. @@ -854,6 +882,7 @@ desde: %s a: %s validador: %s + El monto mínimo para realizar esta transacción es %1$s. Inténtelo de nuevo Ha escaneado la misma tarjeta. Para crear una billetera gemela, necesite escanear la tarjeta con no. %d Ha escaneado la tarjeta gemela incorrecta. Por favor, intente con otra diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 80f7d2e337..cfb3abf10e 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1,5 +1,11 @@ + Jeton non trouvé dans votre portefeuille ? Consultez les marchés pour le trouver et l\'ajouter à l\'achat + Jeton non trouvé dans votre portefeuille ? Consultez les marchés pour le trouver et l\'ajouter à la vente + Choisissez le jeton + Jeton non trouvé dans votre portefeuille ? Consultez les marchés pour le trouver et l\'ajouter à l\'échange + Il n\'y a pas de token disponible pour échanger avec le token sélectionné. Veuillez en choisir un autre. + Aucune paire disponible Choisissez le réseau Ajouter un jeton personnalisé Gérer les jetons @@ -551,9 +557,20 @@ Restauration du code d\'accès Cartes identiques Code d\'accès + Recherche par pays + Indisponible + Recherche par devise + Le montant de l\'achat ne doit pas dépasser %s + Le montant à acheter doit être au moins %s + Aucun fournisseur disponible pour cette devise + Payer avec Nos services ne sont pas disponibles dans ce pays Modifiez-le ou confirmez-le Votre résidence a été identifiée comme + Résidence + Veuillez sélectionner le bon pays pour garantir des options de paiement et des services précis. + Paramètres + Via Grouper Par solde Organiser les jetons @@ -696,6 +713,7 @@ Nom Le montant à staker doit être au moins %s Le montant du staking sera arrondi à %1$s TRX en raison des règles du réseau. + Le montant d\'annulation du staking sera arrondi à %1$s TRX en raison des règles du réseau. Réclamation déstakée Frais de staking du compte Un compte de staking est un compte spécial où sont stockés les jetons SOL stakés. Il est créé lorsque vous déléguez vos jetons à un validateur pour participer à la validation des transactions et gagner des récompenses. Des frais minimes sont facturés pour la création du compte de staking, qui sont restitués une fois le staking terminé. @@ -738,10 +756,13 @@ Le staking vous permet de gagner %1$s. Vos récompenses de staking arrivent toutes les semaines. Stakez en toute sécurité et commencez à gagner des récompenses hebdomadaires Gagnez des récompenses de staking + Votre solde stakée sera trop faible pour annuler votre staking. Vous devrez staker davantage pour atteindre le montant minimum pour annuler le staking. + Solde de staking faible L\'option de staking n\'est actuellement pas disponible en raison des conditions du réseau. Veuillez réessayer plus tard. Le staking dans le réseau %1$s avec un nouveau validateur transférera automatiquement tous les fonds précédemment stakés vers ce validateur Réinvestissez vos récompenses gagnées dans le montant que vous avez staké, augmentant ainsi vos gains potentiels. L\'option de restaker vous permet de déplacer vos fonds d\'un validateur à un autre sans avoir besoin de les déstaker. + Vous êtes sur le point de staker l\'intégralité de votre solde. Nous vous recommandons de laisser un petit montant pour couvrir les frais de réseau pour unstaking ou la réclamation des récompenses. Débloquez votre argent pour le retirer du processus de staking. Le déverrouillage prend %s. Vos fonds seront disponibles à l\'utilisation après la période de déblocage de 21 jours. La récompense sera retirée en même temps que vos fonds de déblocage. Vos fonds seront disponibles pour utilisation après la période de désengagement %s. @@ -770,6 +791,7 @@ Récompenses Stake verrouillé Staker plus + Montant staké Vous stakez %1$s et recevrez %2$s Appuyez pour déverrouiller Appuyez pour déverrouiller ou voter @@ -780,6 +802,7 @@ Dissociation Débloquer Déverrouillage + Le montant à staker doit être au moins %s Le montant dépasse le solde misé Non-staké Unstaking @@ -806,7 +829,9 @@ Échangez plus de jetons à de meilleurs taux directement dans votre portefeuille. Nouveau fournisseur d\'échange disponible ! Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %s depuis l\'échange vers l\'adresse de l\'utilisateur. + Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %1$s depuis l\'échange vers l\'adresse de l\'utilisateur. \n\nLe slippage du fournisseur peut atteindre %2$s Le montant comprend les frais du fournisseur de services. + Le montant comprend les frais du fournisseur de services. \n\nLe slippage du fournisseur peut atteindre %s Frais Tous les échanges décentralisés nécessitent des approbations pour empêcher les smart contracts d\'accéder à votre portefeuille sans votre permission. Par conception, les smart contracts ne peuvent pas accéder à vos jetons sans votre approbation. En « déverrouillant » vos jetons, vous autorisez le smart contract 1-inch à les dépenser. Les mineurs du réseau reçoivent des frais de gaz (payés par vous) pour enregistrer cette action sur la blockchain. Vous pouvez échanger votre jeton après avoir donné votre approbation. Approuver @@ -845,6 +870,9 @@ Échangez ce jeton contre un autre moyennant des frais de service de %1$s du %2$s au %3$s février. Échangez avec Changelly, %s frais Échangez maintenant + Non disponible à l\'achat + Indisponible à la vente + Non disponible pour l\'échange de %s contrat : %s Vous n\'avez pas encore de transactions Échec du chargement de l\'historique des transactions.\nCliquez sur le bouton de rechargement pour mettre à jour les informations. @@ -854,6 +882,7 @@ de : %s à : %s validateur : %s + Le montant minimum pour effectuer cette transaction est %1$s. Réessayez Vous avez scanné la même carte. Pour créer un portefeuille jumeau, vous devez scanner la carte portant le numéro %d Vous avez scanné une mauvaise carte jumelle. S\'il vous plaît, essayez-en un autre diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index d5ae3921d5..58687b9a15 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -1,5 +1,11 @@ + ポートフォリオにトークンが見つかりませんか?マーケットから見つけ、買付のために追加してください + ポートフォリオにトークンが見つかりませんか?マーケットから見つけ、売却のために追加してください + トークンを選択 + ポートフォリオにトークンが見つかりませんか?マーケットから見つけ、スワップのために追加してください + 選択したトークンと交換できるトークンがありません。別のトークンを選択してください。 + 利用可能なペアがありません ネットワークを選択 カスタムトークンの追加 トークンの管理 @@ -543,9 +549,22 @@ アクセスコードの復元 同一のカード アクセスコード + 国で検索 + 利用不可 + 通貨で検索 + 買付金額は%s以下にしてください + 買付金額は少なくとも%sである必要があります + この通貨で利用可能なプロバイダーはありません + 支払う + サードパーティプロバイダー%sで取引を完了できます。 + %sにリダイレクトしています... この国では当社のサービスはご利用いただけません 変更または確認 あなたの住居は次のように識別されています + 住居 + 正確なお支払い方法とサービスを確保するため、正しい国を選択してください。 + 設定 + 経由 グループ 残高順 トークンを整理する @@ -686,6 +705,7 @@ 名前 ステーキング金額は %s 以上である必要があります ネットワークルールにより、ステーキング金額は%1$s TRX に切り上げられます。 + ネットワークルールにより、ステーキング解除の量は%1$s TRX に切り上げられます。 ステーキング解除分を請求する ステーキングアカウント手数料 ステーキングアカウントは、ステーキングされたSOLトークンが保管される特別なアカウントです。取引の検証に参加して報酬を得るために、トークンをバリデーターに委任すると、このアカウントが作成されます。ステーキングアカウントの作成には少額の手数料がかかりますが、この手数料はステーキングが完了すると返金されます。 @@ -728,10 +748,13 @@ ステーキングにより%1$sを獲得できます。ステーキング報酬は毎週受け取れます。 安全にステーキングして、報酬を毎週獲得しましょう ステーキング報酬を獲得 + 残りのステーキング残高が少なすぎてステーキングを解除できません。最小のステーキング解除残高を満たすには、さらにステーキングする必要があります。 + ステーキング残高が低いです ネットワークの状態により、現在ステーキングはご利用いただけません。しばらくしてからもう一度お試しください。 新しいバリデーターで%1$sネットワークにステーキングすると、以前にステーキングされた資金はすべてこのバリデーターに自動的に転送されます。 獲得した報酬をステーキングに再投資し、潜在的な収益を増やします。 再ステーキングを使うと、ステーキングを解除することなく、あるバリデータから別のバリデータに資金を移動できます。 + 残高のすべてをステーキングしようとしています。ステーキング解除や報酬請求にかかるネットワーク手数料をカバーするために、少額を残しておくことをお勧めします。 資金をステーキングから引き出すには、ロックを解除してください。ロック解除には%sかかります。 資金は、21日間のロック解除期間後に使用可能になります。報酬は、ロック解除後の資金とともに引き出されます。 %sのステーキング解除期間後、資金はすぐ利用可能となります。 @@ -760,6 +783,7 @@ 報酬 ステーキングはロックされています もっとステーキングする + ステーキング中の金額 %1$sをステーキングし、報酬%2$sを受け取ります タップしてロック解除 タップしてロック解除もしくは投票 @@ -770,6 +794,7 @@ ステーキング解約中 ロック解除 ロック解除中 + ステーキング金額は %s 以上である必要があります 金額がステーキング残高を超えています ステーキングされていない ステーキング解除 @@ -796,7 +821,9 @@ より多くのトークンをより良いレートで、ウォレット内にて直接交換します。 新しいスワッププロバイダーが利用可能になりました! この金額には以下が含まれます:\n- サービスプロバイダーの手数料\n- 取引所からユーザーのアドレスに%s を送り返すためのネットワーク手数料。 + 金額には以下が含まれます: \n • サービス プロバイダーの手数料\n • 取引所からユーザーのアドレスに%1$sを送金するためのネットワーク手数料。 \n\nプロバイダーのスリッページは最大%2$sです この金額には、サービスプロバイダーの手数料が含まれています。 + 金額にはサービスプロバイダーの手数料が含まれます。 \n\nプロバイダーのスリッページは最大%s です 手数料 すべての分散型取引所は、スマートコントラクトがあなたの許可なくウォレットにアクセスするのを防ぐために承認を必要とします。設計上、スマートコントラクトは承認なしでトークンにアクセスできません。トークンを「ロック解除」することで、あなたは1-inchのスマートコントラクトがトークンを使うことを承認します。ネットワークのマイナーは、このアクションをブロックチェーンに記録するためのガス料金(あなたが支払う)を受け取ります。承認後、トークンを交換することができます。 承認 @@ -835,6 +862,9 @@ このトークンは、2月%2$s-%3$s の間、%1$s のサービス手数料で別のトークンと交換できます。 Changellyでスワップ、手数料%s 今すぐスワップ + 買付できません + 売却できません + %sからのスワップは利用できません コントラクト: %s まだ取引はありません 取引履歴の読み込みに失敗しました。\n情報を更新するには、リロードボタンをクリックしてください。 @@ -844,6 +874,8 @@ 送金元: %s 送金先: %s バリデーター: %s + 最小%s + 最小取引金額は%1$sです。 もう一度やり直してください 同じカードをスキャンしました。ツインウォレットを作成するには、番号%dのカードをスキャンする必要があります。 間違ったツインカードをスキャンしました。別のカードをお試しください。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 99a8ff0869..babcd2542e 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1,5 +1,8 @@ + Выберите токен + Нет доступных токенов для обмена с выбранным токеном, пожалуйста, выберите другой. + Нет доступных пар Выберите сеть Добавить токен Валюты @@ -566,6 +569,15 @@ Восстановление кода доступа Идентичные карты Код доступа + Поиск по стране + Недоступно + Поиск по валюте + Сумма покупки не может быть больше, чем %s + Сумма покупки должна составлять минимум %s + Оплата с + Наши сервисы недоступны в данной стране + Настройки + Через Группы По балансу Упорядочить токены @@ -712,6 +724,7 @@ Имя Сумма для стейкинга должна быть не менее %s Согласно правилам сети, сумма стейкинга будет округлена до %1$sTRX. + Сумма для вывода из стейкинга будет округлена до %1$s TRX ввиду особенностей сети. Забрать средства Комиссия за стейкинг аккаунт Стейкинг аккаунт — это специальный счет, на котором хранятся застейканные токены SOL. Он создается при делегировании ваших токенов валидатору для участия в подтверждении транзакций и получении наград. За создание стейкинг аккаунта взимается небольшая комиссия, которая возвращается после завершения стейкинга. @@ -754,10 +767,12 @@ Стейкинг дает возможность вам получать %1$s. Награда будет зачисляться каждую неделю. Стейкайте безопасно и начинайте получать еженедельные награды. Получите награду за стейкинг + Оставшийся застейканный баланс будет слишком мал для вывода. Вам потребуется застейкать больше средств, чтобы достичь минимальной суммы для вывода. Стейкинг временно недоступен из-за проблем в сети. Пожалуйста, попробуйте позже. Стейкинг в сети %1$s с новым валидатором автоматически переведет ваши текущие застейканные средства на него. Реинвестируйте свои заработанные награды в вашу застейканную сумму, увеличивая потенциальный доход Рестейк позволяет вам переместить средства из одного валидатора в другого без необходимости выхода из стейкинга. + Вы собираетесь застейкать весь баланс, рекомендуем оставить небольшую сумму для оплаты комиссии сети при выходе из стейкинга или получении награды. Разблокируйте свои средства, чтобы вывести их из стейкинга. Разблокировка займёт %s. Ваши средства будут доступны для использования после 21-дневного периода отзыва. Награда будет выведена вместе с вашими выводими средствами. Ваши средства будут доступны после %s периода отзыва. @@ -795,6 +810,7 @@ Отзыв Разблокировать Разблокировка + Сумма для вывода из стейкинга должна быть не менее %s Сумма превышает застейканный баланс Вывод из стейкинга Завершение стейкинга @@ -821,8 +837,10 @@ Обменивайте больше токенов по лучшим курсам прямо в вашем кошельке. Новый провайдер обмена! В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя. + В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %1$s от биржи обратно на адрес пользователя \n\nПроскальзывание провайдера составляет до %2$s В сумму включена комиссия провайдера сервиса. - Комиссии + В сумму включена комиссия провайдера сервиса. \n\nПроскальзывание провайдера составляет до %s + Информация Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен. Подтвердить Ошибка расчета комиссии. Пожалуйста, отправьте информацию в поддержку. @@ -860,6 +878,8 @@ Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля. Обмен с Changelly, %s комиссии Обменять + Недоступно для покупки + Недоступно для продажи контракт: %s У вас еще нет транзакций Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию. @@ -869,6 +889,7 @@ от: %s на: %s валидатор: %s + Минимальная сумма транзакции равна %1$s. Попробовать снова Вы отсканировали ту же карту. Для создания twin-кошелька вам необходимо отсканировать карту с номером %d Вы отсканировали не ту twin-карту. Пожалуйста, попробуйте отсканировать другую @@ -1006,7 +1027,7 @@ Сеть Солана испытывает высокую нагрузку. Если Ваша транзакция не прошла в течение 2 минут, повторите её отправку. Оповещение сети Солана Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату. - Некоторые сети в настоящее время недоступны. Пожалуйста, повторите попытку позже. + Свайпните вниз для обновления или попробуйте позже. Некоторые сети недоступны Это Testnet карта. Он не может обрабатывать транзакции и используется только в целях тестирования и разработки. Только для целей тестирования diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 73f301ed3c..53de8082e3 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1,5 +1,11 @@ + Токен не знайдено у вашому портфелі? Перевірте Ринки, щоб знайти та додати його для покупки + Токен не знайдено у вашому портфелі? Перевірте Ринки, щоб знайти та додати його для продажу + Оберіть токен + Токен не знайдено у вашому портфелі? Перевірте ринки, щоб знайти та додати його для обміну + Немає доступних токенів для обміну з обраним токеном. Будь ласка, оберіть інший. + Немає вільної пари Оберіть мережу Додати токен Токени @@ -520,7 +526,7 @@ Ваша seed-фраза - слово + %d слова %d слів %d слів @@ -567,9 +573,16 @@ Відновлення коду доступу Ідентичні картки Код доступу + Пошук за країною + Недоступно + Пошук по валюті + Оплата з Наші сервіси недоступні в цій країні Змінити або підтвердити Ваше місце проживання визначено як + Будь ласка, виберіть правильну країну, щоб забезпечити точні способи оплати та послуги. + Параметри + Через Групами За балансом Сортування токенів @@ -716,6 +729,7 @@ Ім\'я Сума для стейкінгу має бути не менше %s Сума стейкінгу буде округлена до %1$s TRX відповідно до правил мережі. + Сума зняття зі стейкінгу буде округлена до %1$s TRX через мережеві правила. Зняти кошти Комісія за стейкінг-акаунт Стейкінг-акаунт - це спеціальний рахунок, на якому зберігаються застейкані SOL токени. Він створюється, коли ви делегуєте свої токени валідатору для участі у перевірці транзакцій та отримання винагород. За створення стейкінг-акаунту стягується невелика комісія, яка повертається після завершення стейкінгу. @@ -758,10 +772,13 @@ Стейкінг дає змогу вам отримувати %1$s. Винагорода буде зараховуватися кожен тиждень. Стейкайте безпечно та почніть отримувати винагороди щотижня Отримуйте винагороду за стейкінг + Ваш баланс стейкінгу, що залишився, буде занадто низьким, щоб вивести його зі стейкінгу. Вам потрібно буде застейкати більше, щоб досягти мінімальної суми для виводу. + Низький баланс стейкінгу Стейкінг тимчасово недоступний через проблеми в мережі. Будь ласка, спробуйте пізніше. Стейкінг в мережі %1$s з новим валідатором автоматично переведе всі раніше застейкані кошти до цього валідатора Реінвестуйте зароблені винагороди у суму стейкінгу, щоб збільшити потенційний прибуток. Рестейк дозволяє вам перемістити ваші кошти від одного валідатора до іншого без необхідності виводити кошти зі стейкінгу + Ви збираєтеся застейкати весь свій баланс. Ми рекомендуємо залишити невелику суму, щоб покрити комісію мережі за зняття коштів або отримання винагороди. Розблокуйте свої кошти, щоб вивести їх зі стейкінгу. Розблокування займе %s. Ваші кошти будуть доступні для використання після закінчення 21-денного періоду розблокування. Винагорода буде отримана разом з вашими незастейканими коштами. Ваші кошти будуть доступні після %s періоду розблокування. @@ -790,6 +807,7 @@ Винагороди Стейкінг закрито Застейкати більше + Застейканий залишок Ви стейкаєте %1$s і будете отримуватиме винагороду %2$s Натисніть, щоб розблокувати Натисніть, щоб розблокувати або проголосувати @@ -800,6 +818,7 @@ Розблокування Розблокувати Розблокування + Сума для стейкінгу має бути не менше %s Сума перевищує баланс стейкінгу Вивід зі стейкінгу Зняти зі стейкінгу @@ -826,7 +845,9 @@ Обмінюйте більше токенів за вигіднішим курсом прямо у своєму гаманці. З\'явився новий провайдер обмінів! Сума включає: \n• комісію постачальника послуг\n• комісію мережі за відправлення %s з біржі назад на адресу користувача. + У суму входить:\n- комісія провайдера\n- мережева комісія за відправку %1$s з біржі назад на адресу користувача. \n\nПроскакування провайдера становить до %2$s Сума включає комісію постачальника послуг. + Сума включає комісію провайдера послуг. \n\nПроскакування провайдера до %s Комісії Всі децентралізовані біржі вимагають схвалення, щоб запобігти доступу смарт-контрактів до вашого гаманця без вашого дозволу. За задумом смарт-контракти не можуть отримати доступ до ваших токенів без вашого схвалення. \"Розблоковуючи\" свої токени, ви дозволяєте смарт-контракту 1inch витрачати ваші активи. Майнери мережі отримують плату за газ (сплачену вами), щоб зафіксувати цю дію в блокчейні. Ви можете обміняти свій токен після того, як дасте дозвіл. Підтвердити @@ -865,6 +886,9 @@ Обмінюйте цей токен на інші з %1$s комісією за обслуговування з %2$s по %3$s лютого. Обмін із Changelly, %s комісії Обміняти + Недоступно для покупки + Недоступно для продажу + Недоступно для обміну з %s контракт: %s У вас ще немає транзакцій Не вдалося завантажити історію транзакцій.\nНатисніть кнопку нижче, щоб оновити інформацію @@ -874,6 +898,7 @@ від: %s до: %s валідатор: %s + Мінімальна сума транзакції становить %1$s. Спробуйте знову Ви відсканували одну й ту саму картку. Для створення twin-гаманця вам потрібно відсканувати картку з номером %d Ви відсканували не ту twin-картку. Будь ласка, спробуйте відсканувати іншу @@ -1011,7 +1036,7 @@ Мережа Солана зазнає високого навантаження. Якщо транзакція не пройшла протягом 2 хвилин, повторіть транзакцію. Оповіщення мережі Солана Мережа Solana стягує орендну плату у розмірі %1$s кожні 2 дні. Акаунти, які не можуть дозволити собі орендну плату, видаляються з мережі. Поповніть свій рахунок на суму понад %2$s, щоб не платити орендну плату. - Деякі мережі наразі недоступні. Будь ласка, спробуйте пізніше. + Проведіть пальцем вниз для оновлення або спробуйте пізніше. Деякі мережі недоступні Це картка Testnet. Вона не може обробляти транзакції і повинна використовуватися лише для тестування та розробки. Лише для цілей тестування diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 9b1c743f98..ef0cd2f4f5 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1,5 +1,11 @@ + Token not found in your portfolio? Check the Markets to find and add it for purchase + Token not found in your portfolio? Check the Markets to find and add it for sell + Choose the Token + Token not found in your portfolio? Check the Markets to find and add it for swap + There are no available tokens to swap with the selected token. Please choose another one. + No available pair Choose network Add custom token Manage tokens @@ -551,9 +557,22 @@ Access code restore Identical cards Access code + Search by country + Unavailable + Search by currency + The purchase amount should be no more than %s + The amount to buy must be at least %s + No available providers for this currency + Pay with + You will be able to complete your transaction on the third-party provider, %s + Redirecting to %s... Our services are not available in this country Change or confirm it Your residence has been identified as + Residence + Please select the correct country to ensure accurate payment options and services. + Settings + Via Group By balance Organize tokens @@ -696,6 +715,7 @@ Name The amount to stake must be at least %s Staking amount will be rounded to %1$s TRX due to network rules. + Unstaking amount will be rounded to %1$s TRX due to network rules. Claim unstaked Stake account fee A staking account is a special account where staked SOL tokens are stored. It is created when you delegate your tokens to a validator to participate in transaction validation and earn rewards. A small fee is charged for creating the staking account, which is returned after the staking is completed. @@ -738,10 +758,13 @@ Staking allows you to earn %1$s. Your staking rewards arrive every week. Stake securely and start earning weekly rewards Earn staking rewards + Your remaining staked balance will be too low to unstake. You’ll need to stake more to meet the minimum unstake amount. + Low staked balance Staking is currently unavailable due to network conditions. Please try again later. Staking in the %1$s network with a new validator will automatically transfer all previously staked funds to this validator Reinvests your earned rewards in your staked amount, increasing potential earnings. Restake lets you move your funds from one validator to another without the need to unstake + You’re about to stake your entire balance. We recommend leaving a small amount to cover network fees for unstaking or claiming rewards. Unlock your money to withdraw it from staking process. Unlocking takes %s. Your funds will be available for use after the 21-day unbonding period. Reward will be withdrawn along with your unstaking funds. Your funds will be available for use after the %s unbonding period. @@ -770,6 +793,7 @@ Rewards Stake locked Stake more + Staked amount You stake %1$s and will be receiving your reward %2$s Tap to unlock Tap to unlock or vote @@ -780,6 +804,7 @@ Unbonding Unlock Unlocking + The amount to unstake must be at least %s Amount exceeds staked balance Unstaked Unstaking @@ -806,8 +831,10 @@ Exchange more tokens at better rates directly in your wallet. New Swap Provider Available! The amount includes:\n• service provider\'s fee\n• network fee for sending %s from the exchange back to the user\'s address. + The amount includes:\n• service provider\'s fee\n• network fee for sending %1$s from the exchange back to the user\'s address. \n\nProvider slippage is up to %2$s The amount includes the service provider\'s fee. - Fees + The amount includes the service provider\'s fee. \n\nProvider slippage is up to %s + Information All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval. Approve Fee estimation error. Please send feedback to support. @@ -845,6 +872,9 @@ Exchange this token for another at %1$s service fees from February %2$s-%3$s. Swap with Changelly, %s fees Swap now + Unavailable to purchase + Unavailable to sell + Unavailable for swap from %s contract: %s You don\'t have any transactions yet Failed to load transaction history.\nClick on reload button to update the information. @@ -854,6 +884,8 @@ from: %s to: %s validator: %s + Minimum %s + The minimum transaction amount is %1$s. Try again You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d You\'ve scanned wrong twin card. Please try another one @@ -989,7 +1021,7 @@ The Solana network is congested. If your transaction is not processed within 2 minutes, please repeat the transaction. Solana Network Alert Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free. - Some networks currently are unreachable. Please try again later. + Swipe down to refresh or try again later. Some networks are unreachable This is a Testnet card. It cannot process transactions and should only be used for testing and development purposes. For testing purposes only diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 3781bde624..c7fb1b5984 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -52,4 +52,9 @@ dependencies { api(deps.jodatime) implementation(deps.timber) implementation(deps.markdown) + + /** Tests */ + testImplementation(deps.test.junit) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt index 089d050c7d..8d1787952d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt @@ -53,6 +53,7 @@ internal fun ContentIcon( ) CurrencyIconState.Loading, CurrencyIconState.Locked, + is CurrencyIconState.Empty, -> Unit } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt index d06e59fc30..1b92960c51 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt @@ -1,15 +1,19 @@ package com.tangem.core.ui.components.currency.icon +import androidx.annotation.DrawableRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.getGreyScaleColorFilter @@ -33,6 +37,7 @@ fun CurrencyIcon(state: CurrencyIconState, modifier: Modifier = Modifier, should when (state) { is CurrencyIconState.Loading -> LoadingIcon(modifier = iconModifier) is CurrencyIconState.Locked -> LockedIcon(modifier = iconModifier) + is CurrencyIconState.Empty -> EmptyIcon(resId = state.resId, modifier = iconModifier) is CurrencyIconState.CoinIcon, is CurrencyIconState.CustomTokenIcon, is CurrencyIconState.TokenIcon, @@ -66,6 +71,24 @@ private fun LockedIcon(modifier: Modifier = Modifier) { } } +@Composable +private fun EmptyIcon(@DrawableRes resId: Int, modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.button.secondary, + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(resId), + contentDescription = null, + modifier = Modifier.size(size = 24.dp), + tint = TangemTheme.colors.icon.informative, + ) + } +} + @Composable private fun BoxScope.ContentIconContainer( icon: CurrencyIconState, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt index f19975ab9e..371ad59f61 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt @@ -3,6 +3,7 @@ package com.tangem.core.ui.components.currency.icon import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.R /** * Represents the various states an icon can be in. @@ -82,6 +83,14 @@ sealed class CurrencyIconState { override val topBadgeIconResId: Int? = null } + data class Empty( + @DrawableRes val resId: Int = R.drawable.ic_empty_64, + ) : CurrencyIconState() { + override val isGrayscale: Boolean = true + override val showCustomBadge: Boolean = false + override val topBadgeIconResId: Int? = null + } + fun copySealed( isGrayscale: Boolean = this.isGrayscale, showCustomBadge: Boolean = this.showCustomBadge, @@ -103,6 +112,7 @@ sealed class CurrencyIconState { ) is Loading, is Locked, + is Empty, -> this } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt index c331fc8a1d..6c221c5b10 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/converter/CryptoCurrencyToIconStateConverter.kt @@ -10,8 +10,12 @@ import com.tangem.utils.converter.Converter /** * Converts [CryptoCurrencyStatus] to [CurrencyIconState] + * + * @property isAvailable flag that indicates if the currency is available (affects on icon's grayscale) */ -class CryptoCurrencyToIconStateConverter : Converter { +class CryptoCurrencyToIconStateConverter( + private val isAvailable: Boolean = true, +) : Converter { override fun convert(value: CryptoCurrencyStatus): CurrencyIconState { return when (val currency = value.currency) { @@ -57,7 +61,7 @@ class CryptoCurrencyToIconStateConverter : Converter Unit)? = null, snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, @@ -109,9 +126,6 @@ fun TangemBottomSheetScaffold( sheetSwipeEnabled = sheetSwipeEnabled, shape = sheetShape, containerColor = sheetContainerColor, - contentColor = sheetContentColor, - tonalElevation = sheetTonalElevation, - shadowElevation = sheetShadowElevation, content = sheetContent, ) }, @@ -178,9 +192,6 @@ private fun StandardBottomSheet( sheetSwipeEnabled: Boolean, shape: Shape, containerColor: Color, - contentColor: Color, - tonalElevation: Dp, - shadowElevation: Dp, content: @Composable ColumnScope.() -> Unit, ) { val scope = rememberCoroutineScope() @@ -202,7 +213,7 @@ private fun StandardBottomSheet( Modifier } - Surface( + Column( modifier = Modifier .widthIn(max = sheetMaxWidth) .fillMaxWidth() @@ -251,16 +262,20 @@ private fun StandardBottomSheet( state = state.anchoredDraggableState, orientation = orientation, enabled = sheetSwipeEnabled, - ), - shape = shape, - color = containerColor, - contentColor = contentColor, - tonalElevation = tonalElevation, - shadowElevation = shadowElevation, + ) + .softLayerShadow( + radius = 8.dp, + color = Color.Black.copy( + alpha = if (isSystemInDarkTheme()) .16f else .08f + ), + shape = shape, + offset = DpOffset(x = 0.dp, y = (-4).dp), + isAlphaContentClip = true + ) + .background(containerColor, shape) + .clip(shape), ) { - Column(Modifier.fillMaxWidth()) { - content() - } + content() } } @@ -293,7 +308,7 @@ private fun BottomSheetScaffoldLayout( ), ) { (topBarMeasurables, bodyMeasurables, bottomSheetMeasurables, snackbarHostMeasurables), - constraints, + constraints, -> val layoutWidth = constraints.maxWidth val layoutHeight = constraints.maxHeight @@ -321,7 +336,7 @@ private fun BottomSheetScaffoldLayout( PartiallyExpanded -> sheetOffset().roundToInt() - snackbarHeight Expanded, Hidden, - -> layoutHeight - snackbarHeight + -> layoutHeight - snackbarHeight } // Placement order is important for elevation diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt index e8317b0c26..5a927b0f76 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenFiatAmount.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState as TokenFiatAmountState @@ -24,12 +25,19 @@ import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { when (state) { is TokenFiatAmountState.Content -> { - FiatAmountText( + ContentFiatAmount( text = state.text.orMaskWithStars(isBalanceHidden), hasStaked = state.hasStaked, modifier = modifier, ) } + is TokenItemState.FiatAmountState.TextContent -> { + FiatAmountText( + text = state.text.orMaskWithStars(isBalanceHidden), + modifier = modifier, + isAvailable = state.isAvailable, + ) + } is TokenFiatAmountState.Loading -> { RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) } @@ -41,7 +49,7 @@ internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Bool } @Composable -private fun FiatAmountText(text: String, hasStaked: Boolean, modifier: Modifier = Modifier) { +private fun ContentFiatAmount(text: String, hasStaked: Boolean, modifier: Modifier = Modifier) { Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically, @@ -58,16 +66,23 @@ private fun FiatAmountText(text: String, hasStaked: Boolean, modifier: Modifier .size(TangemTheme.dimens.size12), ) } - Text( - text = text, - color = TangemTheme.colors.text.primary1, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = TangemTheme.typography.body2, - ) + + FiatAmountText(text = text) } } +@Composable +private fun FiatAmountText(text: String, modifier: Modifier = Modifier, isAvailable: Boolean = true) { + Text( + text = text, + modifier = modifier, + color = if (isAvailable) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography.body2, + ) +} + private fun Modifier.placeholderSize(): Modifier = composed { return@composed this .padding(vertical = TangemTheme.dimens.spacing4) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenPrice.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenPrice.kt index aacba7c8b7..f2986b6e11 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenPrice.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenPrice.kt @@ -37,8 +37,12 @@ internal fun TokenPrice(state: TokenPriceState?, modifier: Modifier = Modifier) priceChangePercent = state.priceChangePercent, ) } - is TokenPriceState.TextContent -> PriceText(text = state.value, modifier = modifier) - is TokenPriceState.Unknown -> PriceText(text = DASH_SIGN, modifier = modifier) + is TokenPriceState.TextContent -> { + PriceText(text = state.value, modifier = modifier, isAvailable = state.isAvailable) + } + is TokenPriceState.Unknown -> { + PriceText(text = DASH_SIGN, modifier = modifier) + } is TokenPriceState.Loading -> { RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) } @@ -71,11 +75,11 @@ private fun PriceBlock( } @Composable -private fun PriceText(text: String, modifier: Modifier = Modifier) { +private fun PriceText(text: String, modifier: Modifier = Modifier, isAvailable: Boolean = true) { AnimatedContent(targetState = text, label = "Update the price text", modifier = modifier) { animatedText -> Text( text = animatedText, - color = TangemTheme.colors.text.tertiary, + color = if (isAvailable) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.tertiary, maxLines = 1, overflow = TextOverflow.Ellipsis, style = TangemTheme.typography.caption2, @@ -154,7 +158,7 @@ private class TokenPriceChangeStateProvider : CollectionPreviewParameterProvider priceChangePercent = "2.5%", type = PriceChangeType.NEUTRAL, ), - TokenPriceState.TextContent(value = "Subtitle"), + TokenPriceState.TextContent(value = "Subtitle", isAvailable = true), TokenPriceState.Unknown, TokenPriceState.Loading, TokenPriceState.Locked, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt index 706120f20e..9bf3412f33 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenTitle.kt @@ -22,7 +22,7 @@ import com.tangem.core.ui.components.token.state.TokenItemState.TitleState as To internal fun TokenTitle(state: TokenTitleState?, modifier: Modifier = Modifier) { when (state) { is TokenTitleState.Content -> { - ContentTitle(name = state.text, hasPending = state.hasPending, modifier = modifier) + ContentTitle(state = state, modifier = modifier) } is TokenTitleState.Loading -> { RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4) @@ -35,7 +35,7 @@ internal fun TokenTitle(state: TokenTitleState?, modifier: Modifier = Modifier) } @Composable -private fun ContentTitle(name: String, hasPending: Boolean, modifier: Modifier = Modifier) { +private fun ContentTitle(state: TokenTitleState.Content, modifier: Modifier = Modifier) { Row( modifier = modifier, horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing6), @@ -45,21 +45,25 @@ private fun ContentTitle(name: String, hasPending: Boolean, modifier: Modifier = * If currency name has a long width, then it will completely displace the image. * So we need to use [weight] to avoid displacement. */ - CurrencyNameText(name = name, modifier = Modifier.weight(weight = 1f, fill = false)) + CurrencyNameText( + name = state.text, + isAvailable = state.isAvailable, + modifier = Modifier.weight(weight = 1f, fill = false), + ) PendingTransactionImage( - hasPending = hasPending, + hasPending = state.hasPending, modifier = Modifier.align(alignment = Alignment.CenterVertically), ) } } @Composable -private fun CurrencyNameText(name: String, modifier: Modifier = Modifier) { +private fun CurrencyNameText(name: String, isAvailable: Boolean, modifier: Modifier = Modifier) { Text( text = name, modifier = modifier, - color = TangemTheme.colors.text.primary1, + color = if (isAvailable) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.tertiary, overflow = TextOverflow.Ellipsis, maxLines = 1, style = TangemTheme.typography.subtitle2, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt index 528978ea61..c33e5c8caa 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt @@ -160,7 +160,11 @@ sealed class TokenItemState { @Immutable sealed class TitleState { - data class Content(val text: String, val hasPending: Boolean = false) : TitleState() + data class Content( + val text: String, + val hasPending: Boolean = false, + val isAvailable: Boolean = true, + ) : TitleState() data object Loading : TitleState() @@ -176,7 +180,7 @@ sealed class TokenItemState { val type: PriceChangeType, ) : SubtitleState() - data class TextContent(val value: String) : SubtitleState() + data class TextContent(val value: String, val isAvailable: Boolean = true) : SubtitleState() data object Unknown : SubtitleState() @@ -192,6 +196,8 @@ sealed class TokenItemState { val hasStaked: Boolean = false, ) : FiatAmountState() + data class TextContent(val text: String, val isAvailable: Boolean = true) : FiatAmountState() + data object Loading : FiatAmountState() data object Locked : FiatAmountState() diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt new file mode 100644 index 0000000000..56b0ce8bf1 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt @@ -0,0 +1,39 @@ +package com.tangem.core.ui.components.tokenlist + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.components.token.TokenItem +import com.tangem.core.ui.components.tokenlist.internal.NetworkTitleItem +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.resolveReference + +/** + * Multi-currency content item + * + * @param state component UI model + * @param isBalanceHidden flag that shows/hides balance + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@Composable +fun TokenListItem(state: TokensListItemUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { + when (state) { + is TokensListItemUM.NetworkGroupTitle -> { + NetworkTitleItem(networkName = state.name.resolveReference(), modifier = modifier) + } + is TokensListItemUM.Token -> { + TokenItem( + state = state.state, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + ) + } + is TokensListItemUM.SearchBar -> { + SearchBar(state = state.searchBarUM, modifier = modifier.padding(all = 12.dp)) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkTitleItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/NetworkTitleItem.kt similarity index 89% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkTitleItem.kt rename to core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/NetworkTitleItem.kt index 5bd9e5e129..4f6bf25949 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkTitleItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/internal/NetworkTitleItem.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.common.component +package com.tangem.core.ui.components.tokenlist.internal import android.content.res.Configuration import androidx.compose.foundation.background @@ -17,21 +17,34 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import com.tangem.core.ui.R import com.tangem.core.ui.components.rows.NetworkTitle import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.wallet.impl.R import org.burnoutcrew.reorderable.ReorderableLazyListState import org.burnoutcrew.reorderable.detectReorder import org.burnoutcrew.reorderable.rememberReorderableLazyListState +/** + * Network title item + * + * @param networkName network name + * @param modifier modifier + */ @Composable internal fun NetworkTitleItem(networkName: String, modifier: Modifier = Modifier) { BaseNetworkTitleItem(networkName = networkName, modifier = modifier) } +/** + * Draggable network title item + * + * @param networkName network name + * @param reorderableTokenListState reorderable token list state + * @param modifier modifier + */ @Composable -internal fun DraggableNetworkTitleItem( +fun DraggableNetworkTitleItem( networkName: String, reorderableTokenListState: ReorderableLazyListState, modifier: Modifier = Modifier, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt new file mode 100644 index 0000000000..8a26f00508 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt @@ -0,0 +1,42 @@ +package com.tangem.core.ui.components.tokenlist.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.TextReference + +/** Tokens list item state */ +@Immutable +sealed interface TokensListItemUM { + + /** Unique ID */ + val id: Any + + /** + * Search bar item + * + * @property id id + * @property searchBarUM search bar UI model + */ + data class SearchBar( + override val id: Any = "search_bar", + val searchBarUM: SearchBarUM, + ) : TokensListItemUM + + /** + * Network group title + * + * @property id id + * @property name network group name + */ + data class NetworkGroupTitle(override val id: Int, val name: TextReference) : TokensListItemUM + + /** + * Token item + * + * @property state token state + */ + data class Token(val state: TokenItemState) : TokensListItemUM { + override val id: String = state.id + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt index dabc6f97e2..02615d9faa 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/decorations/RoundedDecorations.kt @@ -1,11 +1,13 @@ package com.tangem.core.ui.decorations +import androidx.compose.foundation.background import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp import com.tangem.core.ui.res.TangemTheme @@ -15,46 +17,49 @@ fun Modifier.roundedShapeItemDecoration( lastIndex: Int, addDefaultPadding: Boolean = true, radius: Dp = TangemTheme.dimens.radius16, + backgroundColor: Color? = null, ): Modifier = composed { val modifier = if (addDefaultPadding) this.padding(horizontal = TangemTheme.dimens.spacing16) else this + + val applyTopPadding: @Composable Modifier.() -> Modifier = { + if (addDefaultPadding) { + padding(top = TangemTheme.dimens.spacing12) + } else { + this + } + } + + val applyShape: Modifier.(shape: RoundedCornerShape?) -> Modifier = { shape -> + if (backgroundColor != null) { + if (shape != null) { + background(color = backgroundColor, shape = shape) + } else { + background(color = backgroundColor) + } + } else { + if (shape != null) { + clip(shape = shape) + } else { + this + } + } + } + val isSingleItem = currentIndex == 0 && lastIndex == 0 when { isSingleItem -> { modifier - .then( - if (addDefaultPadding) { - Modifier.padding(top = TangemTheme.dimens.spacing12) - } else { - Modifier - }, - ) - .clip(shape = RoundedCornerShape(radius)) + .applyTopPadding() + .applyShape(RoundedCornerShape(radius)) } currentIndex == 0 -> { modifier - .then( - if (addDefaultPadding) { - Modifier.padding(top = TangemTheme.dimens.spacing12) - } else { - Modifier - }, - ) - .clip( - shape = RoundedCornerShape( - topStart = radius, - topEnd = radius, - ), - ) + .applyTopPadding() + .applyShape(RoundedCornerShape(topStart = radius, topEnd = radius)) } currentIndex == lastIndex -> { - modifier - .clip( - shape = RoundedCornerShape( - bottomStart = radius, - bottomEnd = radius, - ), - ) + modifier.applyShape(RoundedCornerShape(bottomStart = radius, bottomEnd = radius)) } - else -> modifier + else -> modifier.applyShape(null) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index 9c460e02a1..28de4a0d24 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -79,86 +79,9 @@ fun getActiveIconRes(blockchainId: String): Int { "energy-web-chain", "energy-web-chain/test" -> R.drawable.img_energy_web_22 "energy-web-x", "energy-web-x/test" -> R.drawable.img_energy_web_22 "core", "core/test" -> R.drawable.img_core_22 - else -> R.drawable.ic_alert_24 - } -} - -@Suppress("ComplexMethod", "LongMethod") -@DrawableRes -fun getActiveIconResByNetworkId(networkId: String): Int { - return when (networkId) { - "arbitrum-one", "arbitrum-one/test" -> R.drawable.img_arbitrum_22 - "avalanche", "avalanche-2", "avalanche/test", "avalanche-2/test" -> R.drawable.img_avalanche_22 - "binance-smart-chain", "binance-smart-chain/test", "binancecoin", "binancecoin/test" -> R.drawable.img_bsc_22 - "bitcoin", "bitcoin/test" -> R.drawable.img_btc_22 - "bitcoin-cash", "bitcoin-cash/test" -> R.drawable.img_btc_cash_22 - "litecoin", "litecoin/test" -> R.drawable.img_litecoin_22 - "ethereum", "ethereum/test" -> R.drawable.img_eth_22 - "ethereum-classic", "ethereum-classic/test" -> R.drawable.img_eth_classic_22 - "rootstock" -> R.drawable.img_rsk_22 - "cardano", "cardano/test" -> R.drawable.img_cardano_22 - "tezos" -> R.drawable.img_tezos_22 - "xrp", "ripple" -> R.drawable.img_xrp_22 - "stellar", "stellar/test" -> R.drawable.img_stellar_22 - "polygon-pos", "polygon-pos/test" -> R.drawable.img_polygon_22 - "solana", "solana/test" -> R.drawable.img_solana_22 - "fantom", "fantom/test" -> R.drawable.img_fantom_22 - "dogecoin" -> R.drawable.img_dogecoin_22 - "tron", "tron/test" -> R.drawable.img_tron_22 - "xdai" -> R.drawable.img_gnosis_22 - "ethereum-pow-iou", "ethereum-pow-iou/test" -> R.drawable.img_eth_pow_22 - "ethereumfair", "dischain" -> R.drawable.img_dischain_22 - "polkadot", "polkadot/test" -> R.drawable.img_polkadot_22 - "kusama" -> R.drawable.img_kusama_22 - "optimistic-ethereum", "optimistic-ethereum/test" -> R.drawable.img_optimism_22 - "dash" -> R.drawable.img_dash_22 - "kaspa" -> R.drawable.img_kaspa_22 - "the-open-network", "the-open-network/test" -> R.drawable.img_ton_22 - "kava", "kava/test" -> R.drawable.img_kava_22 - "ravencoin", "ravencoin/test" -> R.drawable.img_ravencoin_22 - "cosmos", "cosmos/test" -> R.drawable.img_cosmos_22 - "terra", "terra-luna" -> R.drawable.img_terra_22 - "terra-2", "terra-luna-2" -> R.drawable.img_terra2_22 - "cronos" -> R.drawable.img_cronos_22 - "telos", "telos/test" -> R.drawable.img_telos_22 - "aleph-zero", "aleph-zero/test" -> R.drawable.img_azero_22 - "octaspace", "octaspace/test" -> R.drawable.img_octaspace_22 - "chia", "chia/test" -> R.drawable.img_chia_22 - "near-protocol", "near-protocol/test" -> R.drawable.img_near_22 - "decimal", "decimal/test" -> R.drawable.img_decimal_22 - "xdc-network", "xdc-network/test" -> R.drawable.img_xdc_22 - "vechain", "vechain/test" -> R.drawable.img_vechain_22 - "aptos", "aptos/test" -> R.drawable.img_aptos_22 - "shibarium", "shibarium/test" -> R.drawable.img_shibarium_22 - "algorand", "algorand/test" -> R.drawable.img_algorand_22 - "hedera-hashgraph", "hedera/test" -> R.drawable.img_hedera_22 - "playa3ull-games" -> R.drawable.img_playa3ull_22 - "ducatus" -> R.drawable.img_ducatus_22 - "aurora", "aurora/test" -> R.drawable.img_aurora_22 - "areon", "areon/test" -> R.drawable.img_areon_22 - "pls", "pls/test" -> R.drawable.img_pls_22 - "zksync", "zksync/test" -> R.drawable.img_zksync_22 - "moonbeam", "moonbeam/test" -> R.drawable.img_moonbeam_22 - "manta-pacific", "manta-pacific/test" -> R.drawable.img_manta_22 - "polygon-zkevm", "polygon-zkevm/test" -> R.drawable.img_polygon_22 - "moonriver", "moonriver/test" -> R.drawable.img_moonriver_22 - "mantle", "mantle/test" -> R.drawable.img_mantle_22 - "flare-network", "flare-network/test" -> R.drawable.img_flare_22 - "taraxa", "taraxa/test" -> R.drawable.img_taraxa_22 - "radiant" -> R.drawable.img_radiant_22 - "base" -> R.drawable.img_base_22 - "joystream" -> R.drawable.img_joystream_22 - "koinos", "koinos/test" -> R.drawable.img_koinos_22 - "bittensor" -> R.drawable.img_bittensor_22 - "blast", "blast/test" -> R.drawable.img_blast_22 - "filecoin" -> R.drawable.img_filecoin_22 - "cyber", "cyber/test" -> R.drawable.img_cyber_22 - "sei", "sei/test" -> R.drawable.img_sei_22 - "internet-computer" -> R.drawable.img_icp_22 - "sui", "sui/test" -> R.drawable.img_sui_22 - "energy-web-chain", "energy-web-chain/test" -> R.drawable.img_energy_web_22 - "energy-web-x", "energy-web-x/test" -> R.drawable.img_energy_web_22 - "core", "core/test" -> R.drawable.img_core_22 + "casper", "casper/test" -> R.drawable.img_casper_22 + "xodex" -> R.drawable.img_xodex_22 + "canxium" -> R.drawable.img_canxium_22 else -> R.drawable.ic_alert_24 } } @@ -176,7 +99,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "ethereum-classic" -> R.drawable.img_eth_classic_22 "stellar" -> R.drawable.img_stellar_22 "cardano" -> R.drawable.img_cardano_22 - "matic-network" -> R.drawable.img_polygon_22 + "matic-network", "polygon-ecosystem-token" -> R.drawable.img_polygon_22 "avalanche-2" -> R.drawable.img_avalanche_22 "solana" -> R.drawable.img_solana_22 "fantom" -> R.drawable.img_fantom_22 @@ -236,6 +159,9 @@ fun getActiveIconResByCoinId(coinId: String): Int { "energy-web-chain", "energy-web-chain/test" -> R.drawable.img_energy_web_22 "energy-web-x", "energy-web-x/test" -> R.drawable.img_energy_web_22 "core", "core/test" -> R.drawable.img_core_22 + "casper-network" -> R.drawable.img_casper_22 + "xodex" -> R.drawable.img_xodex_22 + "canxium" -> R.drawable.img_canxium_22 else -> R.drawable.ic_alert_24 } } @@ -316,86 +242,9 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "energy-web-chain", "energy-web-chain/test" -> R.drawable.ic_energy_web_22 "energy-web-x", "energy-web-x/test" -> R.drawable.ic_energy_web_22 "core", "core/test" -> R.drawable.ic_core_22 - else -> R.drawable.ic_alert_24 - } -} - -@Suppress("ComplexMethod", "LongMethod") -@DrawableRes -fun getGreyedOutIconResByNetworkId(networkId: String): Int { - return when (networkId) { - "arbitrum-one", "arbitrum-one/test" -> R.drawable.ic_arbitrum_22 - "avalanche", "avalanche-2", "avalanche/test", "avalanche-2/test" -> R.drawable.ic_avalanche_22 - "binance-smart-chain", "binance-smart-chain/test", "binancecoin", "binancecoin/test" -> R.drawable.ic_bsc_16 - "bitcoin", "bitcoin/test" -> R.drawable.ic_bitcoin_16 - "bitcoin-cash", "bitcoin-cash/test" -> R.drawable.ic_bitcoin_cash_16 - "litecoin", "litecoin/test" -> R.drawable.ic_litecoin_22 - "ethereum", "ethereum/test" -> R.drawable.ic_eth_16 - "ethereum-classic", "ethereum-classic/test" -> R.drawable.ic_eth_16 - "rootstock" -> R.drawable.ic_rsk_16 - "cardano", "cardano/test" -> R.drawable.ic_cardano_16 - "tezos" -> R.drawable.ic_tezos_16 - "xrp", "ripple" -> R.drawable.ic_xrp_22 - "stellar", "stellar/test" -> R.drawable.ic_stellar_16 - "polygon-pos", "polygon-pos/test" -> R.drawable.ic_polygon_22 - "solana", "solana/test" -> R.drawable.ic_solana_16 - "fantom", "fantom/test" -> R.drawable.ic_fantom_22 - "dogecoin" -> R.drawable.ic_dogecoin_16 - "tron", "tron/test" -> R.drawable.ic_tron_22 - "xdai" -> R.drawable.ic_gnosis_22 - "ethereum-pow-iou", "ethereum-pow-iou/test" -> R.drawable.ic_ethereumpow_22 - "ethereumfair", "dischain" -> R.drawable.ic_dischain_22 - "polkadot", "polkadot/test" -> R.drawable.ic_polkadot_16 - "kusama" -> R.drawable.ic_kusama_16 - "optimistic-ethereum", "optimistic-ethereum/test" -> R.drawable.ic_optimism_22 - "dash" -> R.drawable.ic_dash_22 - "kaspa" -> R.drawable.ic_kaspa_22 - "the-open-network", "the-open-network/test" -> R.drawable.ic_ton_22 - "kava", "kava/test" -> R.drawable.ic_kava_22 - "ravencoin", "ravencoin/test" -> R.drawable.ic_ravencoin_22 - "cosmos", "cosmos/test" -> R.drawable.ic_cosmos_22 - "terra", "terra-luna" -> R.drawable.ic_terra_22 - "terra-2", "terra-luna-2" -> R.drawable.ic_terra2_22 - "cronos" -> R.drawable.ic_cronos_22 - "telos", "telos/test" -> R.drawable.ic_telos_22 - "aleph-zero", "aleph-zero/test" -> R.drawable.ic_azero_22 - "octaspace", "octaspace/test" -> R.drawable.ic_octaspace_22 - "chia", "chia/test" -> R.drawable.ic_chia_22 - "near-protocol", "near-protocol/test" -> R.drawable.ic_near_22 - "decimal", "decimal/test" -> R.drawable.ic_decimal_22 - "xdc-network", "xdc-network/test" -> R.drawable.ic_xdc_22 - "vechain", "vechain/test" -> R.drawable.ic_vechain_22 - "aptos", "aptos/test" -> R.drawable.ic_aptos_22 - "shibarium", "shibarium/test" -> R.drawable.ic_shibarium_22 - "algorand", "algorand/test" -> R.drawable.ic_algorand_22 - "hedera-hashgraph", "hedera/test" -> R.drawable.ic_hedera_22 - "playa3ull-games" -> R.drawable.ic_playa3ull_22 - "ducatus" -> R.drawable.ic_ducatus_22 - "aurora", "aurora/test" -> R.drawable.ic_aurora_22 - "areon", "areon/test" -> R.drawable.ic_areon_22 - "pls", "pls/test" -> R.drawable.ic_pls_22 - "zksync", "zksync/test" -> R.drawable.ic_zksync_22 - "moonbeam", "moonbeam/test" -> R.drawable.ic_moonbeam_22 - "manta-pacific", "manta-pacific/test" -> R.drawable.ic_manta_22 - "polygon-zkevm", "polygon-zkevm/test" -> R.drawable.ic_polygon_22 - "moonriver", "moonriver/test" -> R.drawable.ic_moonriver_22 - "mantle", "mantle/test" -> R.drawable.ic_mantle_22 - "flare-network", "flare-network/test" -> R.drawable.ic_flare_22 - "taraxa", "taraxa/test" -> R.drawable.ic_taraxa_22 - "radiant" -> R.drawable.ic_radiant_22 - "base", "base/test" -> R.drawable.ic_base_22 - "joystream" -> R.drawable.ic_joystream_22 - "koinos", "koinos/test" -> R.drawable.ic_koinos_22 - "bittensor" -> R.drawable.ic_bittensor_22 - "blast", "blast/test" -> R.drawable.ic_blast_22 - "filecoin" -> R.drawable.ic_filecoin_22 - "cyber", "cyber/test" -> R.drawable.ic_cyber_22 - "sei", "sei/test" -> R.drawable.ic_sei_22 - "internet-computer" -> R.drawable.ic_icp_22 - "sui", "sui/test" -> R.drawable.ic_sui_22 - "energy-web-chain", "energy-web-chain/test" -> R.drawable.ic_energy_web_22 - "energy-web-x", "energy-web-x/test" -> R.drawable.ic_energy_web_22 - "core", "core/test" -> R.drawable.ic_core_22 + "casper", "casper/test" -> R.drawable.ic_casper_22 + "xodex" -> R.drawable.ic_xodex_22 + "canxium" -> R.drawable.ic_canxium_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/Shadow.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/Shadow.kt new file mode 100644 index 0000000000..bc1ce31389 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/Shadow.kt @@ -0,0 +1,95 @@ +package com.tangem.core.ui.extensions + +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp + +fun Modifier.softLayerShadow( + radius: Dp = 8.dp, + color: Color = Color.Black.copy(alpha = .23f), + shape: Shape = RectangleShape, + spread: Dp = 0.dp, + offset: DpOffset = DpOffset(x = 0.dp, y = 2.dp), + isAlphaContentClip: Boolean = false, +): Modifier = this.drawWithCache { + val radiusPx = radius.toPx() + require(radiusPx > 0.0F) + val paint = Paint().apply { + this.color = color + + asFrameworkPaint().apply { + isDither = true + isAntiAlias = true + + setShadowLayer( + radiusPx, + offset.x.toPx(), + offset.y.toPx(), + color.toArgb(), + ) + } + } + val shapeOutline = shape.createOutline( + size = size, + layoutDirection = LayoutDirection.Rtl, + density = this, + ) + val shapePath = Path().apply { + addOutline(outline = shapeOutline) + } + + val drawShadowBlock: DrawScope.() -> Unit = { + drawIntoCanvas { canvas -> + canvas.withSave { + if (spread.value != 0.0F) { + canvas.scale( + sx = spreadScale( + spread = spread.toPx(), + size = size.width, + ), + sy = spreadScale( + spread = spread.toPx(), + size = size.height, + ), + pivotX = center.x, + pivotY = center.y, + ) + } + + canvas.drawOutline( + outline = shapeOutline, + paint = paint, + ) + } + } + } + + onDrawBehind { + if (isAlphaContentClip) { + clipShadowByPath( + path = shapePath, + block = drawShadowBlock, + ) + } else { + drawShadowBlock() + } + } +} + +@Suppress("UnnecessaryParentheses") +private fun spreadScale(spread: Float, size: Float): Float = 1.0F + ((spread / size) * 2.0F) + +private fun DrawScope.clipShadowByPath(path: Path, block: DrawScope.() -> Unit) { + clipPath( + path = path, + clipOp = ClipOp.Difference, + block = block, + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCompactFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCompactFormat.kt new file mode 100644 index 0000000000..f083a63c47 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCompactFormat.kt @@ -0,0 +1,159 @@ +package com.tangem.core.ui.format.bigdecimal + +import android.icu.text.CompactDecimalFormat +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.NumberFormat +import java.util.Locale + +// == Formatters == + +/** + * Formats the amount in compact format. + * "123456.6" -> "$123.457K" + * "12345.6" -> "$123.046K" + * @param threeDigitsMethod if true, will format the amount always with 3 significant digits + */ +fun BigDecimalFiatFormat.compact(threeDigitsMethod: Boolean = false): BigDecimalFormat = BigDecimalFormat { value -> + if (value < BigDecimal.ONE) { + return@BigDecimalFormat defaultAmount()(value) + } + + val rawAmount = formatCompactAmount( + amount = value, + locale = locale, + threeDigitsMethod = threeDigitsMethod, + ) + + addFiatCurrencySymbolToStringAmount( + amount = rawAmount, + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + locale = locale, + ) +} + +/** + * Formats the amount in compact format. + * "123456.6" -> "ETH 123.457K" + * "12345.6" -> "123.046K ETH" + * @param threeDigitsMethod if true, will format the amount always with 3 significant digits + */ +fun BigDecimalCryptoFormat.compact(threeDigitsMethod: Boolean = false): BigDecimalFormat = BigDecimalFormat { value -> + if (value < BigDecimal.ONE) { + return@BigDecimalFormat defaultAmount()(value) + } + + val rawAmount = formatCompactAmount( + amount = value, + locale = locale, + threeDigitsMethod = threeDigitsMethod, + ) + + addFiatCurrencySymbolToStringAmount( + amount = rawAmount, + fiatCurrencyCode = BigDecimalFormatConstants.usdCurrency.currencyCode, + fiatCurrencySymbol = BigDecimalFormatConstants.usdCurrency.symbol, + locale = locale, + ).replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = BigDecimalFormatConstants.usdCurrency.symbol, + cryptoCurrencySymbol = symbol, + ) +} + +/** + * Formats the amount in compact format. + * ex. "123456.6" -> "123.46K", "12345.6" -> "123.05K" + * Negative amount is not supported! + */ +fun BigDecimalFormatScope.rawCompact(locale: Locale = Locale.getDefault()) = BigDecimalFormat { value -> + if (value < BigDecimal.ZERO) { + return@BigDecimalFormat value.toPlainString() + } + + formatCompactAmount( + amount = value, + locale = locale, + threeDigitsMethod = false, + ) +} + +// == Helpers == + +/** + * "123456.6" -> "123.457K" + * "12345.6" -> "123.046K" + * Negative amount is not supported + * @param threeDigitsMethod if true, will format the amount always with 3 significant digits + * @param scale the number of digits to the right of the decimal point + */ +@Suppress("MagicNumber") +private fun formatCompactAmount( + amount: BigDecimal, + locale: Locale = Locale.getDefault(), + threeDigitsMethod: Boolean = false, +): String { + if (threeDigitsMethod) { + val scaledAmount = amount.setScale(0, RoundingMode.HALF_UP) + val digitsCount = scaledAmount.toString().count() + val digitsToFormat = 6 - when (digitsCount % 3) { + 0 -> 0 + 1 -> 2 + else -> 1 + } + + val formatter = CompactDecimalFormat.getInstance( + locale, + CompactDecimalFormat.CompactStyle.SHORT, + ).apply { + minimumSignificantDigits = 4 + maximumSignificantDigits = digitsToFormat + } + + return formatter.format(scaledAmount) + } else { + val scaledAmount = amount.setScale(0, RoundingMode.HALF_UP) + val digitsCount = scaledAmount.toString().count() + val digitsToFormat = 5 - when (digitsCount % 3) { + 0 -> 0 + 1 -> 2 + else -> 1 + } + + val formatter = CompactDecimalFormat.getInstance( + locale, + CompactDecimalFormat.CompactStyle.SHORT, + ).apply { + minimumSignificantDigits = 2 + maximumSignificantDigits = digitsToFormat + } + + return formatter.format(scaledAmount) + } +} + +/** + * Adds a proper currency symbol for the provided formatted [amount] + * ex. '10.0k" -> "$10.0k", "string" -> "$string" + */ +private fun addFiatCurrencySymbolToStringAmount( + amount: String, + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + locale: Locale = Locale.getDefault(), +): String { + val sampleAmount = BigDecimal.TEN + val currency = getJavaCurrencyByCode(fiatCurrencyCode) + + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + maximumFractionDigits = 0 + minimumFractionDigits = 0 + this.currency = currency + } + + val formatted = formatter.format(sampleAmount) + .replace(currency.getSymbol(locale), fiatCurrencySymbol) + .replace(sampleAmount.toString(), amount) + + return formatted +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt new file mode 100644 index 0000000000..7d6849591c --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt @@ -0,0 +1,260 @@ +package com.tangem.core.ui.format.bigdecimal + +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CRYPTO_FEE_FORMAT_THRESHOLD +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE +import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.FORMAT_THRESHOLD +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE +import com.tangem.utils.extensions.isNotWhitespace +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.NumberFormat +import java.util.Currency +import java.util.Locale + +open class BigDecimalCryptoFormat( + val symbol: String, + val decimals: Int, + val locale: Locale = Locale.getDefault(), + val ignoreSymbolPosition: Boolean = false, +) : BigDecimalFormat { + + override fun invoke(value: BigDecimal): String = defaultAmount()(value) +} + +class BigDecimalCryptoFormatFull( + val cryptoCurrency: CryptoCurrency, + locale: Locale = Locale.getDefault(), +) : BigDecimalCryptoFormat( + symbol = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + locale = locale, +) { + override fun invoke(value: BigDecimal): String = defaultAmount()(value) +} + +// == Initializers == + +fun BigDecimalFormatScope.crypto( + symbol: String, + decimals: Int, + locale: Locale = Locale.getDefault(), +): BigDecimalCryptoFormat { + return BigDecimalCryptoFormat( + symbol = symbol, + decimals = decimals, + locale = locale, + ) +} + +fun BigDecimalFormatScope.crypto( + cryptoCurrency: CryptoCurrency, + ignoreSymbolPosition: Boolean = false, + locale: Locale = Locale.getDefault(), +): BigDecimalCryptoFormat { + return BigDecimalCryptoFormat( + symbol = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + ignoreSymbolPosition = ignoreSymbolPosition, + locale = locale, + ) +} + +// == Formatters == + +fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value -> + if (ignoreSymbolPosition) { + val formatter = NumberFormat.getInstance(locale).apply { + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + formatter.format(value) + 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 + } + formatter.format(value) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + ) + } +} + +fun BigDecimalCryptoFormat.shorted() = BigDecimalFormat { value -> + val formatter = if (value.isMoreThanThreshold()) { + NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = 2 + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + } else { + NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.DOWN + } + } + + formatter.format(value) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + ) +} + +/** + * Format for displaying crypto amounts with their original decimals. + */ +fun BigDecimalCryptoFormat.uncapped() = BigDecimalFormat { value -> + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = decimals + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + formatter.format(value) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + ) +} + +/** + * Format for displaying crypto amounts with a fixed number of decimals. + */ +fun BigDecimalCryptoFormat.anyDecimals(maxDecimals: Int = decimals, minDecimals: Int = decimals) = + BigDecimalFormat { value -> + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = maxDecimals + minimumFractionDigits = minDecimals + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + formatter.format(value) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + ) + } + +/** + * Format for displaying fees. + * If the fee is less than the threshold, it will be displayed as a fixed value "<0.000001 BTC", " + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + if (value.lessThanFeeCryptoThreshold()) { + buildString { + append(CAN_BE_LOWER_SIGN) + append( + formatter + .format(CRYPTO_FEE_FORMAT_THRESHOLD) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + addStartSpace = true, + ), + ) + } + } else { + buildString { + if (canBeLower) { + append(CAN_BE_LOWER_SIGN) + } + append( + formatter.format(value) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + addStartSpace = canBeLower, + ), + ) + } + } +} + +// == Helpers == + +private fun BigDecimal.isMoreThanThreshold() = this > FORMAT_THRESHOLD + +private fun BigDecimal.lessThanFeeCryptoThreshold() = this > BigDecimal.ZERO && this < CRYPTO_FEE_FORMAT_THRESHOLD + +private val usdCurrency = Currency.getInstance(Locale.US) + +// Replaces fiat currency symbol with crypto currency symbol +// with respect to the position of the symbol and whitespace +internal fun String.replaceFiatSymbolWithCrypto( + fiatCurrencySymbol: String, + cryptoCurrencySymbol: String, + addStartSpace: Boolean = false, +): String { + val str = this + if (str.isEmpty()) return str + + return buildString { + when { + str.endsWith(fiatCurrencySymbol) -> { + val withoutSymbol = str.dropLast(fiatCurrencySymbol.length) + + if (cryptoCurrencySymbol.isBlank()) { + return withoutSymbol + } + + val last = withoutSymbol.lastOrNull() ?: return cryptoCurrencySymbol + + append(withoutSymbol) + + if (last.isNotWhitespace()) { + append(CURRENCY_SPACE) + } + + append(cryptoCurrencySymbol) + } + str.startsWith(fiatCurrencySymbol) -> { + if (addStartSpace) { + append(CURRENCY_SPACE) + } + + val withoutSymbol = str.drop(fiatCurrencySymbol.length) + val first = withoutSymbol.firstOrNull() + ?: return cryptoCurrencySymbol + + if (cryptoCurrencySymbol.isBlank()) { + return withoutSymbol + } + + append(cryptoCurrencySymbol) + + if (first.isNotWhitespace()) { + append(CURRENCY_SPACE) + } + + append(withoutSymbol) + } + else -> append(str) + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..1834e10f45 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt @@ -0,0 +1,144 @@ +package com.tangem.core.ui.format.bigdecimal + +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.NumberFormat +import java.util.Locale + +open class BigDecimalFiatFormat( + val fiatCurrencyCode: String, + val fiatCurrencySymbol: String, + val locale: Locale = Locale.getDefault(), +) : BigDecimalFormat { + override fun invoke(value: BigDecimal): String = defaultAmount()(value) +} + +// == Initializers == + +fun BigDecimalFormatScope.fiat( + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + locale: Locale = Locale.getDefault(), +): BigDecimalFiatFormat { + return BigDecimalFiatFormat( + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + locale = locale, + ) +} + +// == Formatters == + +/** + * Formats fiat amount with default precision. + */ +fun BigDecimalFiatFormat.defaultAmount(): BigDecimalFormat = BigDecimalFormat { 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 + } + + if (value.isLessThanThreshold()) { + buildString { + append(CAN_BE_LOWER_SIGN) + append( + formatter.format(FIAT_FORMAT_THRESHOLD) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol), + ) + } + } else { + formatter.format(value) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + } +} + +/** + * Formats fiat amount with default precision and adds tilde sign + */ +fun BigDecimalFiatFormat.approximateAmount(): BigDecimalFormat = BigDecimalFormat { value -> + val formattedAmount = defaultAmount()(value) + + if (value.isLessThanThreshold()) { + formattedAmount + } else { + buildString { + append(TILDE_SIGN) + append(formattedAmount) + } + } +} + +/** + * Formats fiat amount with extended precision. + */ +fun BigDecimalFiatFormat.uncapped(): BigDecimalFormat = BigDecimalFormat { value -> + val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode) + + val digits = if (value.isLessThanThreshold()) { + FIAT_MARKET_EXTENDED_DIGITS + } else { + FIAT_MARKET_DEFAULT_DIGITS + } + + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + maximumFractionDigits = digits + minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + roundingMode = RoundingMode.HALF_UP + } + + formatter.format(value) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) +} + +/** + * Formats fiat price with precision calculated based on the value. + * @see getFiatPriceAmountWithScale + */ +fun BigDecimalFiatFormat.price(): BigDecimalFormat = BigDecimalFormat { value -> + val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode) + + val (priceAmount, finalScale) = getFiatPriceAmountWithScale(value = value) + + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + maximumFractionDigits = finalScale + minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + roundingMode = RoundingMode.HALF_UP + } + + formatter.format(priceAmount) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) +} + +// == Helpers == + +private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD + +private fun getFiatPriceAmountWithScale(value: BigDecimal): Pair { + return if (value < BigDecimal.ONE) { + val leadingZeroes = value.scale() - value.precision() + val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES + + val amount = value + .setScale(scale, RoundingMode.HALF_UP) + .stripTrailingZeros() + + amount to amount.scale() + } else { + value to FIAT_MARKET_DEFAULT_DIGITS + } +} + +// == Constants == + +private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01") +private const val FIAT_MARKET_DEFAULT_DIGITS = 2 +private const val FIAT_MARKET_EXTENDED_DIGITS = 6 +private const val FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES = 4 \ No newline at end of file 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 new file mode 100644 index 0000000000..c685db7f1d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt @@ -0,0 +1,29 @@ +package com.tangem.core.ui.format.bigdecimal + +import java.math.BigDecimal + +interface BigDecimalFormatScope { + companion object { val Empty = object : BigDecimalFormatScope {} } +} + +fun interface BigDecimalFormat : (BigDecimal) -> String, BigDecimalFormatScope + +inline fun BigDecimal.format(block: BigDecimalFormatScope.() -> BigDecimalFormat): String { + return BigDecimalFormatScope.Empty.block()(this) +} + +inline fun BigDecimal?.format( + fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, + block: BigDecimalFormatScope.() -> BigDecimalFormat, +): String { + if (this == null) return 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) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormatConstants.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormatConstants.kt new file mode 100644 index 0000000000..59835ab84c --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormatConstants.kt @@ -0,0 +1,20 @@ +package com.tangem.core.ui.format.bigdecimal + +import com.tangem.utils.StringsSigns.DASH_SIGN +import com.tangem.utils.StringsSigns.LOWER_SIGN +import java.math.BigDecimal +import java.util.Currency +import java.util.Locale + +object BigDecimalFormatConstants { + + const val EMPTY_BALANCE_SIGN = DASH_SIGN + const val CAN_BE_LOWER_SIGN = LOWER_SIGN + val FORMAT_THRESHOLD = BigDecimal("0.01") + + const val CURRENCY_SPACE = '\u00a0' + + val CRYPTO_FEE_FORMAT_THRESHOLD = BigDecimal("0.000001") + + val usdCurrency: Currency by lazy { Currency.getInstance(Locale.US) } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormat.kt new file mode 100644 index 0000000000..b7019d1d3c --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormat.kt @@ -0,0 +1,39 @@ +package com.tangem.core.ui.format.bigdecimal + +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.NumberFormat +import java.util.Locale + +class BigDecimalPercentFormat( + val withoutSign: Boolean = true, + val locale: Locale = Locale.getDefault(), +) : BigDecimalFormat { + override fun invoke(value: BigDecimal): String = default()(value) +} + +// == Initializers == + +fun BigDecimalFormatScope.percent( + withoutSign: Boolean = true, + locale: Locale = Locale.getDefault(), +): BigDecimalPercentFormat { + return BigDecimalPercentFormat( + withoutSign = withoutSign, + locale = locale, + ) +} + +// == Formatters == + +private fun BigDecimalPercentFormat.default(): BigDecimalFormat = BigDecimalFormat { value -> + val formatter = NumberFormat.getPercentInstance(locale).apply { + maximumFractionDigits = 2 + minimumFractionDigits = 2 + roundingMode = RoundingMode.HALF_UP + } + + val valueToFormat = if (withoutSign) value.abs() else value + + formatter.format(valueToFormat) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalSimpleFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalSimpleFormat.kt new file mode 100644 index 0000000000..f6c5067d65 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalSimpleFormat.kt @@ -0,0 +1,34 @@ +package com.tangem.core.ui.format.bigdecimal + +import java.math.BigDecimal +import java.math.RoundingMode +import java.text.NumberFormat +import java.util.Locale + +open class BigDecimalSimpleFormat( + val decimals: Int, + val locale: Locale = Locale.getDefault(), +) : BigDecimalFormat { + + override fun invoke(value: BigDecimal): String = default()(value) +} + +// == Initializers == + +fun BigDecimalFormatScope.simple(decimals: Int, locale: Locale = Locale.getDefault()) = BigDecimalSimpleFormat( + decimals = decimals, + locale = locale, +) + +// == Formatters == + +fun BigDecimalSimpleFormat.default() = BigDecimalFormat { value -> + val formatter = NumberFormat.getInstance(locale).apply { + maximumFractionDigits = decimals + minimumFractionDigits = 0 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + formatter.format(value) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/Utils.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/Utils.kt new file mode 100644 index 0000000000..94509b276f --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/Utils.kt @@ -0,0 +1,15 @@ +package com.tangem.core.ui.format.bigdecimal + +import java.util.Currency + +fun getJavaCurrencyByCode(code: String): Currency { + return runCatching { Currency.getInstance(code) } + .getOrElse { e -> + // Currency code is not valid ISO 4217 code + if (e is IllegalArgumentException) { + BigDecimalFormatConstants.usdCurrency + } else { + throw e + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index 4c44809cfa..744a2d9bf3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -1,28 +1,22 @@ package com.tangem.core.ui.utils -import android.icu.text.CompactDecimalFormat -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.StringsSigns.LOWER_SIGN import com.tangem.utils.StringsSigns.TILDE_SIGN -import com.tangem.utils.extensions.isNotWhitespace -import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode -import java.text.DecimalFormat import java.text.NumberFormat import java.util.Currency import java.util.Locale @Suppress("LargeClass") +@Deprecated("Use BigDecimal.format") object BigDecimalFormatter { const val EMPTY_BALANCE_SIGN = DASH_SIGN private const val CAN_BE_LOWER_SIGN = LOWER_SIGN - private val FORMAT_THRESHOLD = BigDecimal("0.01") private val FIAT_FORMAT_THRESHOLD = BigDecimal("0.01") - private val CRYPTO_FEE_FORMAT_THRESHOLD = BigDecimal("0.000001") private const val FIAT_MARKET_DEFAULT_DIGITS = 2 private const val FIAT_MARKET_EXTENDED_DIGITS = 6 @@ -30,161 +24,7 @@ object BigDecimalFormatter { private val usdCurrency = Currency.getInstance("USD") - @Deprecated( - "Use formatCryptoAmount2", - replaceWith = ReplaceWith("formatCryptoAmount2"), - ) - fun formatCryptoAmount( - cryptoAmount: BigDecimal?, - cryptoCurrency: String, - decimals: Int, - locale: Locale = Locale.getDefault(), - ): String { - if (cryptoAmount == null) return EMPTY_BALANCE_SIGN - - val formatter = NumberFormat.getNumberInstance(locale).apply { - maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) - minimumFractionDigits = 2 - isGroupingUsed = true - roundingMode = RoundingMode.HALF_UP - } - - return formatter.format(cryptoAmount).let { - if (cryptoCurrency.isEmpty()) { - it - } else { - it + "\u2009$cryptoCurrency" - } - } - } - - // Migrate to this method from formatCryptoAmount ([REDACTED_TASK_KEY]) - fun formatCryptoAmount2( - cryptoAmount: BigDecimal?, - cryptoCurrency: String, - decimals: Int, - locale: Locale = Locale.getDefault(), - ): String { - if (cryptoAmount == null) return EMPTY_BALANCE_SIGN - - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = usdCurrency - maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) - minimumFractionDigits = 2 - isGroupingUsed = true - roundingMode = RoundingMode.HALF_UP - } - - return formatter.format(cryptoAmount) - .replaceFiatSymbolWithCrypto( - fiatCurrencySymbol = usdCurrency.symbol, - cryptoCurrencySymbol = cryptoCurrency, - ) - } - - fun formatCryptoAmountShorted( - cryptoAmount: BigDecimal?, - cryptoCurrency: String, - decimals: Int, - locale: Locale = Locale.getDefault(), - ): String { - if (cryptoAmount == null) return EMPTY_BALANCE_SIGN - - val formatter = if (cryptoAmount.isMoreThanThreshold()) { - NumberFormat.getNumberInstance(locale).apply { - maximumFractionDigits = 2 - minimumFractionDigits = 2 - isGroupingUsed = true - roundingMode = RoundingMode.HALF_UP - } - } else { - NumberFormat.getNumberInstance(locale).apply { - maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6) - minimumFractionDigits = 2 - isGroupingUsed = true - roundingMode = RoundingMode.DOWN - } - } - - return formatter.format(cryptoAmount).let { - if (cryptoCurrency.isEmpty()) { - it - } else { - it + "\u2009$cryptoCurrency" - } - } - } - - fun formatCryptoAmountUncapped( - cryptoAmount: BigDecimal?, - cryptoCurrency: CryptoCurrency, - locale: Locale = Locale.getDefault(), - ): String { - if (cryptoAmount == null) return EMPTY_BALANCE_SIGN - - val formatter = NumberFormat.getNumberInstance(locale).apply { - maximumFractionDigits = cryptoCurrency.decimals - minimumFractionDigits = 2 - isGroupingUsed = true - roundingMode = RoundingMode.HALF_UP - } - - return formatter.format(cryptoAmount).let { - if (cryptoCurrency.symbol.isEmpty()) { - it - } else { - it + "\u2009${cryptoCurrency.symbol}" - } - } - } - - fun formatCryptoFeeAmount( - cryptoAmount: BigDecimal?, - cryptoCurrency: String, - decimals: Int, - canBeLower: Boolean = false, - locale: Locale = Locale.getDefault(), - ): String { - if (cryptoAmount == null) return EMPTY_BALANCE_SIGN - - val formatter = NumberFormat.getNumberInstance(locale).apply { - maximumFractionDigits = decimals.coerceAtMost(maximumValue = 6) - minimumFractionDigits = 2 - isGroupingUsed = true - roundingMode = RoundingMode.HALF_UP - } - - val amountFormatted = if (cryptoAmount.checkCryptoThreshold()) { - buildString { - append(CAN_BE_LOWER_SIGN) - append( - formatter.format(CRYPTO_FEE_FORMAT_THRESHOLD), - ) - } - } else { - buildString { - if (canBeLower) { - append(CAN_BE_LOWER_SIGN) - } - append(formatter.format(cryptoAmount)) - } - } - - return if (cryptoCurrency.isEmpty()) { - amountFormatted - } else { - amountFormatted + "\u2009$cryptoCurrency" - } - } - - fun formatCryptoAmount( - cryptoAmount: BigDecimal?, - cryptoCurrency: CryptoCurrency, - locale: Locale = Locale.getDefault(), - ): String { - return formatCryptoAmount(cryptoAmount, cryptoCurrency.symbol, cryptoCurrency.decimals, locale) - } - + @Deprecated("Use BigDecimal.format") fun formatFiatAmount( fiatAmount: BigDecimal?, fiatCurrencyCode: String, @@ -226,6 +66,7 @@ object BigDecimalFormatter { } } + @Deprecated("Use BigDecimal.format") fun formatFiatAmountUncapped( fiatAmount: BigDecimal?, fiatCurrencyCode: String, @@ -251,6 +92,7 @@ object BigDecimalFormatter { .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) } + @Deprecated("Use BigDecimal.format") fun formatFiatPriceUncapped( fiatAmount: BigDecimal?, fiatCurrencyCode: String, @@ -273,6 +115,7 @@ object BigDecimalFormatter { .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) } + @Deprecated("Use BigDecimal.format") fun getFiatPriceUncappedWithScale(value: BigDecimal): Pair { return if (value < BigDecimal.ONE) { val leadingZeroes = value.scale() - value.precision() @@ -288,47 +131,6 @@ object BigDecimalFormatter { } } - fun formatFiatEditableAmount( - fiatAmount: String?, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - locale: Locale = Locale.getDefault(), - ): String { - if (fiatAmount == null) return EMPTY_BALANCE_SIGN - - val formatterCurrency = getCurrency(fiatCurrencyCode) - val numberFormatter = NumberFormat.getCurrencyInstance(locale).apply { - currency = formatterCurrency - } - val formatter = requireNotNull(numberFormatter as? DecimalFormat) { - Timber.e("NumberFormat is null") - return EMPTY_BALANCE_SIGN - } - return "${formatter.positivePrefix}$fiatAmount${formatter.positiveSuffix}" - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) - } - - fun formatPercent( - percent: BigDecimal, - useAbsoluteValue: Boolean, - locale: Locale = Locale.getDefault(), - maxFractionDigits: Int = 2, - minFractionDigits: Int = 2, - ): String { - val formatter = NumberFormat.getPercentInstance(locale).apply { - maximumFractionDigits = maxFractionDigits - minimumFractionDigits = minFractionDigits - roundingMode = RoundingMode.HALF_UP - } - val value = if (useAbsoluteValue) percent.abs() else percent - - return formatter.format(value) - } - - fun formatWithSymbol(amount: String, symbol: String) = "$amount\u2009$symbol" - - private fun BigDecimal.isMoreThanThreshold() = this > FORMAT_THRESHOLD - private fun getCurrency(code: String): Currency { return runCatching { Currency.getInstance(code) } .getOrElse { e -> @@ -341,231 +143,5 @@ object BigDecimalFormatter { } } - /** - * Adds a proper currency sign for the provided formatted [amount] - * ex. '10.0k" -> "$10.0k", "string" -> "$string" - */ - private fun addCurrencySymbolToStringAmount( - amount: String, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - locale: Locale = Locale.getDefault(), - ): String { - val sampleAmount = BigDecimal.TEN - val currency = getCurrency(fiatCurrencyCode) - - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - maximumFractionDigits = 0 - minimumFractionDigits = 0 - this.currency = currency - } - - val formatted = formatter.format(sampleAmount) - .replace(currency.getSymbol(locale), fiatCurrencySymbol) - .replace(sampleAmount.toString(), amount) - - return formatted - } - - /** - * Adds a proper currency sign for the provided formatted [amount] - * ex. '10.0k" -> "ETH 10.0k", "string" -> "ETH string" - */ - private fun addCryptoCurrencySymbolToStringAmount( - amount: String, - cryptoCurrencySymbol: String, - locale: Locale = Locale.getDefault(), - ): String { - val sampleAmount = BigDecimal.TEN - - val formatter = NumberFormat.getCurrencyInstance(locale).apply { - maximumFractionDigits = 0 - minimumFractionDigits = 0 - currency = usdCurrency - } - - val formatted = formatter.format(sampleAmount) - .replace(sampleAmount.toString(), amount) - - return formatted.replaceFiatSymbolWithCrypto( - fiatCurrencySymbol = usdCurrency.symbol, - cryptoCurrencySymbol = cryptoCurrencySymbol, - ) - } - - /** - * "123456.6" -> "$123.457K" - * "12345.6" -> "$123.046K" - * Negative amount is not supported - * @param threeDigitsMethod if true, will format the amount always with 3 significant digits - * @param scale the number of digits to the right of the decimal point - */ - @Suppress("MagicNumber") - fun formatCompactFiatAmount( - amount: BigDecimal?, - fiatCurrencyCode: String, - fiatCurrencySymbol: String, - threeDigitsMethod: Boolean = false, - scale: Int = 0, - locale: Locale = Locale.getDefault(), - ): String { - if (amount == null) return EMPTY_BALANCE_SIGN - - if (amount < BigDecimal.ONE) { - return formatFiatPriceUncapped( - fiatAmount = amount, - fiatCurrencyCode = fiatCurrencyCode, - fiatCurrencySymbol = fiatCurrencySymbol, - locale = locale, - ) - } - - val rawAmount = formatCompactAmount( - amount = amount, - locale = locale, - threeDigitsMethod = threeDigitsMethod, - scale = scale, - ) - - return addCurrencySymbolToStringAmount( - amount = rawAmount, - fiatCurrencyCode = fiatCurrencyCode, - fiatCurrencySymbol = fiatCurrencySymbol, - locale = locale, - ) - } - - /** - * "123456.6" -> "ETH 123.457K" - * "12345.6" -> "123.046K ETH" - * Negative amount is not supported - * @param threeDigitsMethod if true, will format the amount always with 3 significant digits - * @param scale the number of digits to the right of the decimal point - */ - fun formatCompactCryptoAmount( - amount: BigDecimal?, - cryptoCurrencySymbol: String, - threeDigitsMethod: Boolean = false, - decimals: Int = 0, - locale: Locale = Locale.getDefault(), - ): String { - if (amount == null) return EMPTY_BALANCE_SIGN - - if (amount < BigDecimal.ONE) { - return formatCryptoAmount2( - cryptoAmount = amount, - cryptoCurrency = cryptoCurrencySymbol, - decimals = decimals, - locale = locale, - ) - } - - val rawAmount = formatCompactAmount( - amount = amount, - locale = locale, - threeDigitsMethod = threeDigitsMethod, - scale = decimals, - ) - - return addCryptoCurrencySymbolToStringAmount( - amount = rawAmount, - cryptoCurrencySymbol = cryptoCurrencySymbol, - locale = locale, - ) - } - - /** - * "123456.6" -> "123.457K" - * "12345.6" -> "123.046K" - * Negative amount is not supported - * @param threeDigitsMethod if true, will format the amount always with 3 significant digits - * @param scale the number of digits to the right of the decimal point - */ - @Suppress("MagicNumber") - fun formatCompactAmount( - amount: BigDecimal, - locale: Locale = Locale.getDefault(), - threeDigitsMethod: Boolean = false, - scale: Int = 0, - ): String { - if (threeDigitsMethod) { - val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP) - val digitsCount = scaledAmount.toString().count() - val digitsToFormat = 6 - when (digitsCount % 3) { - 0 -> 0 - 1 -> 2 - else -> 1 - } - - val formatter = CompactDecimalFormat.getInstance( - locale, - CompactDecimalFormat.CompactStyle.SHORT, - ).apply { - minimumSignificantDigits = 4 - maximumSignificantDigits = digitsToFormat - } - - return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP)) - } else { - val scaledAmount = amount.setScale(scale, RoundingMode.HALF_UP) - val digitsCount = scaledAmount.toString().count() - val digitsToFormat = 5 - when (digitsCount % 3) { - 0 -> 0 - 1 -> 2 - else -> 1 - } - - val formatter = CompactDecimalFormat.getInstance( - locale, - CompactDecimalFormat.CompactStyle.SHORT, - ).apply { - minimumSignificantDigits = 2 - maximumSignificantDigits = digitsToFormat - } - - return formatter.format(amount.setScale(scale, RoundingMode.HALF_UP)) - } - } - - // Replaces fiat currency symbol with crypto currency symbol - // with respect to the position of the symbol and whitespace - private fun String.replaceFiatSymbolWithCrypto(fiatCurrencySymbol: String, cryptoCurrencySymbol: String): String { - val str = this - if (str.isEmpty()) return str - - return buildString { - when { - str.endsWith(fiatCurrencySymbol) -> { - val withoutSymbol = str.dropLast(fiatCurrencySymbol.length) - val last = withoutSymbol.lastOrNull() ?: return cryptoCurrencySymbol - - append(withoutSymbol) - - if (last.isNotWhitespace()) { - append("\u2009") - } - - append(cryptoCurrencySymbol) - } - str.startsWith(fiatCurrencySymbol) -> { - append(cryptoCurrencySymbol) - - val withoutSymbol = str.drop(fiatCurrencySymbol.length) - val first = withoutSymbol.firstOrNull() - ?: return cryptoCurrencySymbol - - if (first.isNotWhitespace()) { - append("\u2009") - } - - append(withoutSymbol) - } - else -> append(str) - } - } - } - private fun BigDecimal.checkFiatThreshold() = this > BigDecimal.ZERO && this < FIAT_FORMAT_THRESHOLD - - private fun BigDecimal.checkCryptoThreshold() = this > BigDecimal.ZERO && this < CRYPTO_FEE_FORMAT_THRESHOLD } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_canxium_22.xml b/core/ui/src/main/res/drawable/ic_canxium_22.xml new file mode 100644 index 0000000000..6ec1ea1096 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_canxium_22.xml @@ -0,0 +1,12 @@ + + + + diff --git a/core/ui/src/main/res/drawable/ic_casper_22.xml b/core/ui/src/main/res/drawable/ic_casper_22.xml new file mode 100644 index 0000000000..e2abfc22ea --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_casper_22.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/wallet/impl/src/main/res/drawable/ic_empty_64.xml b/core/ui/src/main/res/drawable/ic_empty_64.xml similarity index 100% rename from features/wallet/impl/src/main/res/drawable/ic_empty_64.xml rename to core/ui/src/main/res/drawable/ic_empty_64.xml diff --git a/core/ui/src/main/res/drawable/ic_xodex_22.xml b/core/ui/src/main/res/drawable/ic_xodex_22.xml new file mode 100644 index 0000000000..bb8c9d880d --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_xodex_22.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_canxium_22.xml b/core/ui/src/main/res/drawable/img_canxium_22.xml new file mode 100644 index 0000000000..7e2e3b0263 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_canxium_22.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_casper_22.xml b/core/ui/src/main/res/drawable/img_casper_22.xml new file mode 100644 index 0000000000..ade71437f7 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_casper_22.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_xodex_22.xml b/core/ui/src/main/res/drawable/img_xodex_22.xml new file mode 100644 index 0000000000..878bf9187f --- /dev/null +++ b/core/ui/src/main/res/drawable/img_xodex_22.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormatTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormatTest.kt new file mode 100644 index 0000000000..a073607b8b --- /dev/null +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormatTest.kt @@ -0,0 +1,407 @@ +package com.tangem.core.ui.format.bigdecimal + +import com.google.common.truth.Truth +import org.junit.Test +import java.math.BigDecimal +import java.util.Locale + +internal class BigDecimalCryptoFormatTest { + + private val testLocale = Locale.US + private val testLocale2 = Locale.GERMANY + private val symbol = "BTC" + + // === defaultAmount() === + + @Test + fun `defaultAmount (usually used as a user balance)`() { + val testValue = BigDecimal("0.123456789999") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 8, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("0.12345679".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `defaultAmount (usually used as a user balance) alter locale`() { + val testValue = BigDecimal("0.123456789999") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 8, + locale = testLocale2, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("0,12345679".addSymbolWithSpaceRight(symbol)) + } + + @Test + fun `defaultAmount decimals more than 8`() { + val testValue = BigDecimal("0.123456789999") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("0.12345679".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `defaultAmount decimals more than 8 (short value)`() { + val testValue = BigDecimal("0.12345") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("0.12345".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `defaultAmount decimals minimal (short value)`() { + val testValue = BigDecimal("0.12345") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 2, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("0.12".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `defaultAmount less than 2 decimals`() { + val testValue = BigDecimal("0.12345") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 0, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("0.12".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `defaultAmount grouping`() { + val testValue = BigDecimal("12345678.11") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 0, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("12,345,678.11".addSymbolWithSpaceLeft(symbol)) + } + + // === shorted() === + + @Test + fun `shorted amount smoke`() { + val testValue = BigDecimal("50000.126123") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 8, + locale = testLocale, + ).shorted() + } + + Truth.assertThat(formatted) + .isEqualTo("50,000.13".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `shorted amount decimals less than 2 grouping`() { + val testValue = BigDecimal("50000.126123") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 1, + locale = testLocale, + ).shorted() + } + + Truth.assertThat(formatted) + .isEqualTo("50,000.13".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `shorted amount less than threshold`() { + val testValue = BigDecimal("0.0034567899") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 4, + locale = testLocale, + ).shorted() + } + + Truth.assertThat(formatted) + .isEqualTo("0.0034".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `shorted amount less than threshold, more decimals`() { + val testValue = BigDecimal("0.00345678") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 8, + locale = testLocale, + ).shorted() + } + + Truth.assertThat(formatted) + .isEqualTo("0.003456".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `shorted amount diff locale half up`() { + val testValue = BigDecimal("50000.126123") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 8, + locale = testLocale2, + ).shorted() + } + + Truth.assertThat(formatted) + .isEqualTo("50.000,13".addSymbolWithSpaceRight(symbol)) + } + + // === uncapped() === + + @Test + fun `uncapped amount`() { + val testValue = BigDecimal("50000.123412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale, + ).uncapped() + } + + Truth.assertThat(formatted) + .isEqualTo("50,000.1234123412".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `uncapped amount diff locale`() { + val testValue = BigDecimal("50000.123412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale2, + ).uncapped() + } + + Truth.assertThat(formatted) + .isEqualTo("50.000,1234123412".addSymbolWithSpaceRight(symbol)) + } + + @Test + fun `uncapped amount half up`() { + val testValue = BigDecimal("50000.12341234125") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale2, + ).uncapped() + } + + Truth.assertThat(formatted) + .isEqualTo("50.000,1234123413".addSymbolWithSpaceRight(symbol)) + } + + @Test + fun `uncapped amount min decimals`() { + val testValue = BigDecimal("50000.12341234125") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 1, + locale = testLocale2, + ).uncapped() + } + + Truth.assertThat(formatted) + .isEqualTo("50.000,12".addSymbolWithSpaceRight(symbol)) + } + + // === fee === + + @Test + fun `fee amount`() { + val testValue = BigDecimal("0.000123412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale, + ).fee() + } + + Truth.assertThat(formatted) + .isEqualTo("0.000123".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `fee amount diff locale`() { + val testValue = BigDecimal("0.000123412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale2, + ).fee() + } + + Truth.assertThat(formatted) + .isEqualTo("0,000123".addSymbolWithSpaceRight(symbol)) + } + + @Test + fun `fee amount canBeLower true`() { + val testValue = BigDecimal("0.000123412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale, + ).fee(canBeLower = true) + } + + Truth.assertThat(formatted) + .isEqualTo("<" + CURRENCY_SPACE_FOR_TESTS + "0.000123".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `fee amount canBeLower true (diff locale)`() { + val testValue = BigDecimal("0.000123412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale2, + ).fee(canBeLower = true) + } + + Truth.assertThat(formatted) + .isEqualTo("<" + "0,000123".addSymbolWithSpaceRight(symbol)) + } + + @Test + fun `fee amount lee than threshold`() { + val testValue = BigDecimal("0.0000001234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 10, + locale = testLocale, + ).fee() + } + + Truth.assertThat(formatted) + .isEqualTo("<" + CURRENCY_SPACE_FOR_TESTS + "0.000001".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `fee amount min decimals half up`() { + val testValue = BigDecimal("0.125412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 1, + locale = testLocale, + ).fee() + } + + Truth.assertThat(formatted) + .isEqualTo("0.13".addSymbolWithSpaceLeft(symbol)) + } + + // === anyDecimals() === + + @Test + fun `anyDecimals smoke`() { + val testValue = BigDecimal("0.123412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 5, + locale = testLocale, + ).anyDecimals() + } + + Truth.assertThat(formatted) + .isEqualTo("0.12341".addSymbolWithSpaceLeft(symbol)) + } + + @Test + fun `anyDecimals zero`() { + val testValue = BigDecimal("0.123412341234") + + val formatted = testValue.format { + crypto( + symbol = symbol, + decimals = 0, + locale = testLocale, + ).anyDecimals() + } + + Truth.assertThat(formatted) + .isEqualTo("0".addSymbolWithSpaceLeft(symbol)) + } +} \ No newline at end of file diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt new file mode 100644 index 0000000000..d5c2cb310e --- /dev/null +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt @@ -0,0 +1,297 @@ +package com.tangem.core.ui.format.bigdecimal + +import com.google.common.truth.Truth +import org.junit.Test +import java.math.BigDecimal +import java.util.Locale + +internal class BigDecimalFiatFormatTest { + + val testLocale = Locale.US + val testLocale2 = Locale.GERMANY + + val usdCurrencyCode = "USD" + val usdSymbol = "$" + + private fun String.addUsdSymbolLeft() = usdSymbol + this + + // === defaultAmount() === + + @Test + fun `defaultAmount smoke`() { + val testValue = BigDecimal("1234.1234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("1,234.12".addUsdSymbolLeft()) + } + + @Test + fun `defaultAmount half up`() { + val testValue = BigDecimal("1234.125") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("1,234.13".addUsdSymbolLeft()) + } + + @Test + fun `defaultAmount diff locale`() { + val testValue = BigDecimal("1234.1234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale2, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("1.234,12".addSymbolWithSpaceRight(usdSymbol)) + } + + @Test + fun `defaultAmount less threshold`() { + val testValue = BigDecimal("0.002234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("<" + "0.01".addUsdSymbolLeft()) + } + + @Test + fun `defaultAmount less threshold diff locale`() { + val testValue = BigDecimal("0.002234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale2, + ).defaultAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("<" + "0,01".addSymbolWithSpaceRight(usdSymbol)) + } + + // === approximateAmount() === + + @Test + fun `approximateAmount smoke`() { + val testValue = BigDecimal("1234.1234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).approximateAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("~" + "1,234.12".addUsdSymbolLeft()) + } + + @Test + fun `approximateAmount half up`() { + val testValue = BigDecimal("1234.125") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).approximateAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("~" + "1,234.13".addUsdSymbolLeft()) + } + + @Test + fun `approximateAmount diff locale`() { + val testValue = BigDecimal("1234.1234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale2, + ).approximateAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("~" + "1.234,12".addSymbolWithSpaceRight(usdSymbol)) + } + + @Test + fun `approximateAmount less threshold`() { + val testValue = BigDecimal("0.002234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).approximateAmount() + } + + Truth.assertThat(formatted) + .isEqualTo("<" + "0.01".addUsdSymbolLeft()) + } + + // === uncapped() === + + @Test + fun `uncapped smoke`() { + val testValue = BigDecimal("1234.1234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).uncapped() + } + + Truth.assertThat(formatted) + .isEqualTo("1,234.12".addUsdSymbolLeft()) + } + + @Test + fun `uncapped less threshold`() { + val testValue = BigDecimal("0.00121") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).uncapped() + } + + Truth.assertThat(formatted) + .isEqualTo("0.00121".addUsdSymbolLeft()) + } + + @Test + fun `uncapped decimals overflow`() { + val testValue = BigDecimal("0.00123412341234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).uncapped() + } + + Truth.assertThat(formatted) + .isEqualTo("0.001234".addUsdSymbolLeft()) + } + + // === price() === + + @Test + fun `price smoke`() { + val testValue = BigDecimal("1234.1234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).price() + } + + Truth.assertThat(formatted) + .isEqualTo("1,234.12".addUsdSymbolLeft()) + } + + @Test + fun `price diff locale`() { + val testValue = BigDecimal("1234.1234") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale2, + ).price() + } + + Truth.assertThat(formatted) + .isEqualTo("1.234,12".addSymbolWithSpaceRight(usdSymbol)) + } + + @Test + fun `price less threshold`() { + val testValue = BigDecimal("0.99987") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).price() + } + + Truth.assertThat(formatted) + .isEqualTo("0.9999".addUsdSymbolLeft()) + } + + @Test + fun `price less threshold more decimals strip zeros`() { + val testValue = BigDecimal("0.0000123000") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).price() + } + + Truth.assertThat(formatted) + .isEqualTo("0.0000123".addUsdSymbolLeft()) + } + + @Test + fun `price less threshold too much decimals strip zeros`() { + val testValue = BigDecimal("0.000000000000000000001230001234000") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).price() + } + + Truth.assertThat(formatted) + .isEqualTo("0.00000000000000000000123".addUsdSymbolLeft()) + } +} \ No newline at end of file diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFormatTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFormatTest.kt new file mode 100644 index 0000000000..63061a9116 --- /dev/null +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFormatTest.kt @@ -0,0 +1,29 @@ +package com.tangem.core.ui.format.bigdecimal + +import com.google.common.truth.Truth +import org.junit.Test +import java.math.BigDecimal + +internal class BigDecimalFormatTest { + + @Test + fun smoke() { + val value = BigDecimal("1234") + val bgformat = BigDecimalFormat { bg -> + bg.toPlainString() + "!" + } + val expected = "1234!" + + Truth.assertThat( + value.format(bgformat), + ).isEqualTo(expected) + + Truth.assertThat( + value.format { bgformat }, + ).isEqualTo(expected) + + Truth.assertThat( + null.format(fallbackString = "!") { bgformat }, + ).isEqualTo("!") + } +} \ No newline at end of file diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormatTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormatTest.kt new file mode 100644 index 0000000000..3d3be8c25f --- /dev/null +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalPercentFormatTest.kt @@ -0,0 +1,70 @@ +package com.tangem.core.ui.format.bigdecimal + +import com.google.common.truth.Truth +import org.junit.Test +import java.math.BigDecimal +import java.util.Locale + +internal class BigDecimalPercentFormatTest { + + val testLocale = Locale.US + val testLocale2 = Locale.GERMANY + + @Test + fun smoke() { + val value = BigDecimal("00.34") + + val formatted = value.format { + percent(locale = testLocale) + } + + Truth.assertThat(formatted).isEqualTo("34.00%") + } + + @Test + fun negative() { + val value = BigDecimal("00.34").negate() + + val formatted = value.format { + percent(locale = testLocale) + } + + Truth.assertThat(formatted).isEqualTo("34.00%") + } + + @Test + fun `negative with sign`() { + val value = BigDecimal("00.34").negate() + + val formatted = value.format { + percent( + withoutSign = false, + locale = testLocale, + ) + } + + Truth.assertThat(formatted).isEqualTo("-34.00%") + } + + @Test + fun `default more decimals half up`() { + val value = BigDecimal("00.345678").negate() + + val formatted = value.format { + percent(locale = testLocale) + } + + Truth.assertThat(formatted).isEqualTo("34.57%") + } + + @Test + fun `default diff locale`() { + val value = BigDecimal("00.345678").negate() + + val formatted = value.format { + percent(locale = testLocale2) + } + + Truth.assertThat(formatted).isEqualTo("34,57".addSymbolWithSpaceRight("%")) + } +} \ No newline at end of file diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/TestUtils.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/TestUtils.kt new file mode 100644 index 0000000000..436873335d --- /dev/null +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/TestUtils.kt @@ -0,0 +1,7 @@ +package com.tangem.core.ui.format.bigdecimal + +internal const val CURRENCY_SPACE_FOR_TESTS = '\u00a0' + +internal fun String.addSymbolWithSpaceRight(symbol: String): String = "$this$CURRENCY_SPACE_FOR_TESTS$symbol" + +internal fun String.addSymbolWithSpaceLeft(symbol: String): String = "$symbol$CURRENCY_SPACE_FOR_TESTS$this" \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt b/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt deleted file mode 100644 index ce1a6d2470..0000000000 --- a/core/utils/src/main/java/com/tangem/utils/CryptoCurrencyFormatExtensions.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.tangem.utils - -import java.math.BigDecimal -import java.math.RoundingMode -import java.text.DecimalFormat -import java.text.NumberFormat -import java.util.Locale - -// todo determine where to place this extensions -fun BigDecimal.toFormattedString( - decimals: Int, - roundingMode: RoundingMode = RoundingMode.DOWN, - locale: Locale = Locale.getDefault(), -): String { - val formatter = NumberFormat.getInstance(locale) as? DecimalFormat - val df = formatter?.apply { - maximumFractionDigits = decimals - minimumFractionDigits = 0 - isGroupingUsed = true - this.roundingMode = roundingMode - } - return df?.format(this) ?: this.toPlainString() -} - -@Suppress("MagicNumber") -fun BigDecimal.toFormattedCurrencyString( - decimals: Int, - currency: String? = null, - roundingMode: RoundingMode = RoundingMode.DOWN, - limitNumberOfDecimals: Boolean = true, -): String { - val decimalsForRounding = if (limitNumberOfDecimals) { - if (decimals > 8) 8 else decimals - } else { - decimals - } - val formattedAmount = this.toFormattedString( - decimals = decimalsForRounding, - roundingMode = roundingMode, - ) - val formattedCurrency = currency?.let { " $it" } ?: "" - return "$formattedAmount$formattedCurrency" -} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt index 991f77b883..b47160124d 100644 --- a/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt +++ b/core/utils/src/main/java/com/tangem/utils/StringsSigns.kt @@ -10,4 +10,5 @@ object StringsSigns { const val TILDE_SIGN = "~" const val INFINITY_SIGN = "∞" const val NON_BREAKING_SPACE = '\u00A0' + const val PERCENT = "%" } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt b/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt index b818478a45..603ebf1fcd 100644 --- a/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt +++ b/core/utils/src/main/java/com/tangem/utils/converter/Converter.kt @@ -11,4 +11,19 @@ interface Converter { fun convertSet(input: Collection): Set { return input.mapTo(hashSetOf(), ::convert) } + + fun convertListIgnoreErrors(input: Collection, onError: ((Throwable) -> Unit)? = null): List { + return input.mapNotNull { + try { + convert(it) + } catch (throwable: Throwable) { + onError?.invoke(throwable) + null + } + } + } + + fun T?.asMandatory(name: String): T { + return this ?: error("$name must not be null") + } } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt index d50f9f295c..6e5a872f02 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/JobHolder.kt @@ -1,6 +1,9 @@ package com.tangem.utils.coroutines +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch /** * Job holder. It is automatically finished old job if new one is started @@ -26,4 +29,13 @@ class JobHolder { fun Job.saveIn(jobHolder: JobHolder): Job = jobHolder.update(job = this) -suspend fun Job.saveInAndJoin(jobHolder: JobHolder) = saveIn(jobHolder).join() \ No newline at end of file +suspend fun Job.saveInAndJoin(jobHolder: JobHolder) = saveIn(jobHolder).join() + +fun CoroutineScope.withDebounce(jobHolder: JobHolder, timeMillis: Long = 800L, function: () -> Unit) { + launch { + delay(timeMillis = timeMillis) + + function() + } + .saveIn(jobHolder) +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/BigDecimalExt.kt b/core/utils/src/main/java/com/tangem/utils/extensions/BigDecimalExt.kt index 31a31d6704..45f2966872 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/BigDecimalExt.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/BigDecimalExt.kt @@ -12,4 +12,7 @@ fun BigDecimal.isZero(): Boolean = this.compareTo(BigDecimal.ZERO) == 0 fun BigDecimal.isPositive(): Boolean = this.signum() == 1 /** Removes trailing zeros and returns plain [String] */ -fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString() \ No newline at end of file +fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString() + +/** Compares two [BigDecimal] numbers */ +infix fun BigDecimal.isEqualTo(other: BigDecimal): Boolean = this.compareTo(other) == 0 \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index 70c7c69396..deb08005ef 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -109,7 +109,7 @@ internal class DefaultManageTokensRepository( ), active = true, searchText = query, - offset = request.offset * request.limit, + offset = request.offset, limit = request.limit, ).getOrThrow() } diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingActionRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingActionRepository.kt new file mode 100644 index 0000000000..888f6a342c --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingActionRepository.kt @@ -0,0 +1,30 @@ +package com.tangem.data.staking + +import com.tangem.datasource.local.token.StakingActionsStore +import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.staking.repositories.StakingActionRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext + +internal class DefaultStakingActionRepository( + private val stakingActionsStore: StakingActionsStore, + private val dispatchers: CoroutineDispatcherProvider, +) : StakingActionRepository { + + override suspend fun store( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + actions: List, + ) { + withContext(dispatchers.io) { + stakingActionsStore.store(userWalletId, cryptoCurrencyId, actions) + } + } + + override fun get(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow> { + return stakingActionsStore.get(userWalletId, cryptoCurrencyId) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingErrorResolver.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingErrorResolver.kt index 34ebad7aa5..a5f5c43f33 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingErrorResolver.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingErrorResolver.kt @@ -26,8 +26,8 @@ internal class DefaultStakingErrorResolver( is StakingError.StakeKitUnknownError -> { analyticsEventHandler.send(StakingAnalyticsEvent.StakeKitApiUnknownError(error)) } - else -> { - // intentionally do nothing + is StakingError.DomainError -> { + analyticsEventHandler.send(StakingAnalyticsEvent.DomainError(error)) } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingPendingTransactionRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingPendingTransactionRepository.kt deleted file mode 100644 index 008ea7161a..0000000000 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingPendingTransactionRepository.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.data.staking - -import com.tangem.domain.staking.model.PendingTransaction -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository -import com.tangem.domain.wallets.models.UserWalletId -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.CopyOnWriteArrayList - -internal class DefaultStakingPendingTransactionRepository : StakingPendingTransactionRepository { - - private val pendingTransactionsMap = ConcurrentHashMap>() - - override fun saveTransaction(userWalletId: UserWalletId, transaction: PendingTransaction) { - val transactions = pendingTransactionsMap.computeIfAbsent(userWalletId) { CopyOnWriteArrayList() } - transactions.add(transaction) - } - - override fun removeTransactions(userWalletId: UserWalletId, transactions: Set) { - pendingTransactionsMap[userWalletId]?.removeAll(transactions) - } - - override fun getTransactionsWithBalanceItems( - userWalletId: UserWalletId, - ): List> { - return pendingTransactionsMap[userWalletId]?.mapNotNull { pendingTransaction: PendingTransaction -> - PendingTransactionItemConverter.convert(pendingTransaction)?.let { balanceItem -> - pendingTransaction to balanceItem - } - } ?: emptyList() - } -} \ No newline at end of file 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 b7e900c83b..3261381510 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -2,7 +2,6 @@ package com.tangem.data.staking import android.util.Base64 import arrow.core.getOrElse -import arrow.core.raise.catch import com.squareup.moshi.Moshi import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain @@ -26,21 +25,19 @@ import com.tangem.data.staking.converters.transaction.StakingTransactionTypeConv import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.request.* +import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.stakekit.NetworkType -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.staking.model.stakekit.YieldBalanceList +import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate @@ -57,6 +54,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import kotlinx.coroutines.withContext import timber.log.Timber @@ -103,11 +101,9 @@ internal class DefaultStakingRepository( private val yieldBalanceConverter = YieldBalanceConverter() private val yieldBalanceListConverter = YieldBalanceListConverter(yieldBalanceConverter) - private val isYieldBalanceFetching = MutableStateFlow( - value = emptyMap(), - ) - private val tronStakeKitTransactionAdapter by lazy { moshi.adapter(TronStakeKitTransaction::class.java) } + private val networkTypeAdapter by lazy { moshi.adapter(NetworkTypeDTO::class.java) } + private val stakingActionStatusAdapter by lazy { moshi.adapter(StakingActionStatusDTO::class.java) } override fun getIntegrationKey(cryptoCurrencyId: CryptoCurrency.ID): String = with(cryptoCurrencyId) { rawNetworkId.plus(rawCurrencyId) @@ -126,7 +122,7 @@ internal class DefaultStakingRepository( val stakingTokensWithYields = stakeKitApi.getEnabledYields(preferredValidatorsOnly = false) .getOrThrow() - stakingYieldsStore.store(stakingTokensWithYields.data.filter { it.isAvailable }) + stakingYieldsStore.store(stakingTokensWithYields.data.filter { it.isAvailable ?: false }) }, ) } @@ -146,12 +142,46 @@ internal class DefaultStakingRepository( } } + override suspend fun getActions( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + networkType: NetworkType, + stakingActionStatus: StakingActionStatus, + ): List { + return withContext(dispatchers.io) { + val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() + + val networkTypeDto = networkTypeConverter.convertBack(networkType) + val networkTypeString = networkTypeDto.extractJsonName() + + val actionStatusDTO = actionStatusConverter.convertBack(stakingActionStatus) + val actionStatusString = actionStatusDTO.extractJsonName() + + enterActionResponseConverter.convertListIgnoreErrors( + input = stakeKitApi.getActions( + walletAddress = address, + network = networkTypeString, + status = actionStatusString, + ).getOrThrow().data, + onError = { Timber.e("Error converting staking actions list: $it") }, + ) + } + } + + private fun NetworkTypeDTO.extractJsonName(): String { + return networkTypeAdapter.toJson(this).replace("\"", "") + } + + private fun StakingActionStatusDTO.extractJsonName(): String { + return stakingActionStatusAdapter.toJson(this).replace("\"", "") + } + override suspend fun getEntryInfo(cryptoCurrencyId: CryptoCurrency.ID, symbol: String): StakingEntryInfo { return withContext(dispatchers.io) { val yield = getYield(cryptoCurrencyId, symbol) StakingEntryInfo( - apr = requireNotNull(yield.validators.maxByOrNull { it.apr.orZero() }?.apr), + apr = requireNotNull(yield.preferredValidators.maxByOrNull { it.apr.orZero() }?.apr), rewardSchedule = yield.metadata.rewardSchedule, tokenSymbol = yield.token.symbol, ) @@ -207,23 +237,21 @@ internal class DefaultStakingRepository( ): StakingAction { return withContext(dispatchers.io) { val response = when (params.actionCommonType) { - StakingActionCommonType.ENTER -> stakeKitApi.createEnterAction( + StakingActionCommonType.Enter -> stakeKitApi.createEnterAction( createActionRequestBody( userWalletId, network, params, ), ) - StakingActionCommonType.EXIT -> stakeKitApi.createExitAction( + StakingActionCommonType.Exit -> stakeKitApi.createExitAction( createActionRequestBody( userWalletId, network, params, ), ) - StakingActionCommonType.PENDING_OTHER, - StakingActionCommonType.PENDING_REWARDS, - -> stakeKitApi.createPendingAction( + is StakingActionCommonType.Pending -> stakeKitApi.createPendingAction( createPendingActionRequestBody(params), ) } @@ -239,23 +267,21 @@ internal class DefaultStakingRepository( ): StakingGasEstimate { return withContext(dispatchers.io) { val gasEstimateDTO = when (params.actionCommonType) { - StakingActionCommonType.ENTER -> stakeKitApi.estimateGasOnEnter( + StakingActionCommonType.Enter -> stakeKitApi.estimateGasOnEnter( createActionRequestBody( userWalletId, network, params, ), ) - StakingActionCommonType.EXIT -> stakeKitApi.estimateGasOnExit( + StakingActionCommonType.Exit -> stakeKitApi.estimateGasOnExit( createActionRequestBody( userWalletId, network, params, ), ) - StakingActionCommonType.PENDING_REWARDS, - StakingActionCommonType.PENDING_OTHER, - -> stakeKitApi.estimateGasOnPending( + is StakingActionCommonType.Pending -> stakeKitApi.estimateGasOnPending( createPendingActionRequestBody(params), ) } @@ -389,88 +415,66 @@ internal class DefaultStakingRepository( refresh: Boolean, ) = withContext(dispatchers.io) { if (!stakingFeatureToggle.isStakingEnabled) return@withContext - try { - isYieldBalanceFetching.update { - it + (userWalletId to true) - } - cacheRegistry.invokeOnExpire( - key = getYieldBalancesKey(userWalletId), - skipCache = refresh, - block = { - val yields = getEnabledYields() - val availableCurrencies = cryptoCurrencies - .mapNotNull { currency -> - val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network) - val integrationId = integrationIdMap[getIntegrationKey(currency.id)] - if (integrationId != null && yields.any { it.id == integrationId }) { - addresses to integrationId - } else { - null - } - } - .flatMap { (addresses, integrationId) -> - addresses.map { address -> address to integrationId } - } - .map { getBalanceRequestData(it.first.value, it.second) } - .ifEmpty { - cacheRegistry.invalidate(getYieldBalancesKey(userWalletId)) - error("No addresses found") - } - val result = stakeKitApi.getMultipleYieldBalances(availableCurrencies).getOrThrow() + cacheRegistry.invokeOnExpire( + key = getYieldBalancesKey(userWalletId), + skipCache = refresh, + block = { + val yields = getEnabledYields().ifEmpty { + Timber.i("No enabled yields for $userWalletId") + stakingBalanceStore.store(userWalletId, emptySet()) - stakingBalanceStore.store(userWalletId, result) - }, - ) - } finally { - isYieldBalanceFetching.update { - it - userWalletId - } - } + return@invokeOnExpire + } + val availableCurrencies = cryptoCurrencies + .mapNotNull { currency -> + val addresses = walletManagersFacade.getAddresses(userWalletId, currency.network) + val integrationId = integrationIdMap[getIntegrationKey(currency.id)] + + if (integrationId != null && yields.any { it.id == integrationId }) { + addresses to integrationId + } else { + null + } + } + .flatMap { (addresses, integrationId) -> + addresses.map { address -> address to integrationId } + } + .map { getBalanceRequestData(it.first.value, it.second) } + .ifEmpty { + Timber.i("No yield balances available for $userWalletId") + stakingBalanceStore.store(userWalletId, emptySet()) + + cacheRegistry.invalidate(getYieldBalancesKey(userWalletId)) + + return@invokeOnExpire + } + + val result = stakeKitApi + .getMultipleYieldBalances(availableCurrencies) + .getOrThrow() + + stakingBalanceStore.store(userWalletId, result) + }, + ) } - override fun getMultiYieldBalanceFlow( + override fun getMultiYieldBalanceUpdates( userWalletId: UserWalletId, cryptoCurrencies: List, ): Flow = channelFlow { if (!stakingFeatureToggle.isStakingEnabled) { send(YieldBalanceList.Empty) } else { - launch(dispatchers.io) { - stakingBalanceStore.get(userWalletId) - .collectLatest { send(yieldBalanceListConverter.convert(it)) } - } + stakingBalanceStore.get(userWalletId) + .onEach { + val balances = yieldBalanceListConverter.convert(it) + send(balances) + } + .launchIn(scope = this + dispatchers.io) withContext(dispatchers.io) { - fetchMultiYieldBalance( - userWalletId, - cryptoCurrencies, - ) - } - } - }.cancellable() - - override fun getMultiYieldBalanceLce( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): LceFlow = lceFlow { - if (!stakingFeatureToggle.isStakingEnabled) { - send(YieldBalanceList.Empty) - } else { - launch(dispatchers.io) { - combine( - stakingBalanceStore.get(userWalletId), - isYieldBalanceFetching.map { it.getOrElse(userWalletId) { false } }, - ) { result, isFetching -> - val balances = yieldBalanceListConverter.convert(result) - send(balances, isStillLoading = isFetching) - }.collect() - } - withContext(dispatchers.io) { - catch( - block = { fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false) }, - catch = { raise(it) }, - ) + fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false) } } } @@ -583,9 +587,10 @@ internal class DefaultStakingRepository( } private fun getEnabledYields(): List { - return stakingYieldsStore - .get() - .map { yieldConverter.convert(it) } + return yieldConverter.convertListIgnoreErrors( + input = stakingYieldsStore.get(), + onError = { Timber.e("Error converting one of the items in enabled yields: $it") }, + ) } private fun getBalanceRequestData(address: String, integrationId: String): YieldBalanceRequestBody { @@ -640,7 +645,7 @@ internal class DefaultStakingRepository( Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID, Blockchain.Ethereum.id + Blockchain.Polygon.toMigratedCointId() to ETHEREUM_POLYGON_INTEGRATION_ID, // Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID, - // Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID, + Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID, // Blockchain.Polkadot.run { id + toCoinId() } to POLKADOT_INTEGRATION_ID, // Blockchain.Avalanche.run { id + toCoinId() } to AVALANCHE_INTEGRATION_ID, // Blockchain.Cronos.run { id + toCoinId() } to CRONOS_INTEGRATION_ID, diff --git a/data/staking/src/main/java/com/tangem/data/staking/PendingTransactionItemConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/PendingTransactionItemConverter.kt deleted file mode 100644 index 026c1435bd..0000000000 --- a/data/staking/src/main/java/com/tangem/data/staking/PendingTransactionItemConverter.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.data.staking - -import com.tangem.domain.staking.model.PendingTransaction -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.utils.converter.Converter -import org.joda.time.DateTime - -internal object PendingTransactionItemConverter : Converter { - - override fun convert(value: PendingTransaction): BalanceItem? { - return BalanceItem( - groupId = value.groupId ?: return null, - token = value.token, - type = value.type, - amount = value.amount, - rawCurrencyId = value.rawCurrencyId, - validatorAddress = value.validator?.address, - date = DateTime.now(), - pendingActions = emptyList(), - isPending = true, - ) - } -} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt index 1ef336d4f8..6d6b14b10b 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -17,37 +17,37 @@ class YieldConverter( override fun convert(value: YieldDTO): Yield { return Yield( - id = value.id, - token = tokenConverter.convert(value.token), - tokens = value.tokens.map { tokenConverter.convert(it) }, - args = convertArgs(value.args), - status = convertStatus(value.status), - apy = value.apy, - rewardRate = value.rewardRate, - rewardType = convertRewardType(value.rewardType), - metadata = convertMetadata(value.metadata), - validators = value.validators + id = value.id.asMandatory("id"), + token = tokenConverter.convert(value.token.asMandatory("token")), + tokens = value.tokens.asMandatory("tokens").map { tokenConverter.convert(it) }, + args = convertArgs(value.args.asMandatory("args")), + status = convertStatus(value.status.asMandatory("status")), + apy = value.apy.asMandatory("apy"), + rewardRate = value.rewardRate.asMandatory("rewardRate"), + rewardType = convertRewardType(value.rewardType.asMandatory("rewardType")), + metadata = convertMetadata(value.metadata.asMandatory("metadata")), + validators = value.validators.asMandatory("validators") .asSequence() .filter { it.status == ValidatorStatusDTO.ACTIVE } .map { convertValidator(it) } .sortedByDescending { it.isStrategicPartner } .sortedByDescending { it.apr } .toImmutableList(), - isAvailable = value.isAvailable, + isAvailable = value.isAvailable.asMandatory("isAvailable"), ) } private fun convertArgs(argsDTO: YieldDTO.ArgsDTO): Yield.Args { return Yield.Args( - enter = convertEnter(argsDTO.enter), + enter = convertEnter(argsDTO.enter.asMandatory("enter")), exit = argsDTO.exit?.let { convertEnter(it) }, ) } private fun convertEnter(enterDTO: YieldDTO.ArgsDTO.Enter): Yield.Args.Enter { return Yield.Args.Enter( - addresses = convertAddresses(enterDTO.addresses), - args = enterDTO.args + addresses = convertAddresses(enterDTO.addresses.asMandatory("addresses")), + args = enterDTO.args.asMandatory("args") .mapKeys { convertArgType(it.key) } .mapValues { convertAddressArgument(it.value) }, ) @@ -55,7 +55,7 @@ class YieldConverter( private fun convertAddresses(addressesDTO: YieldDTO.ArgsDTO.Enter.Addresses): Yield.Args.Enter.Addresses { return Yield.Args.Enter.Addresses( - address = convertAddressArgument(addressesDTO.address), + address = convertAddressArgument(addressesDTO.address.asMandatory("address")), additionalAddresses = addressesDTO.additionalAddresses ?.mapKeys { convertArgType(it.key) } ?.mapValues { convertAddressArgument(it.value) }, @@ -73,58 +73,61 @@ class YieldConverter( private fun convertStatus(statusDTO: YieldDTO.StatusDTO): Yield.Status { return Yield.Status( - enter = statusDTO.enter, + enter = statusDTO.enter.asMandatory("enter"), exit = statusDTO.exit, ) } private fun convertMetadata(metadataDTO: YieldDTO.MetadataDTO): Yield.Metadata { return Yield.Metadata( - name = metadataDTO.name, - logoUri = metadataDTO.logoUri, - description = metadataDTO.description, + name = metadataDTO.name.asMandatory("name"), + logoUri = metadataDTO.logoUri.asMandatory("logoUri"), + description = metadataDTO.description.asMandatory("description"), documentation = metadataDTO.documentation, - gasFeeToken = tokenConverter.convert(metadataDTO.gasFeeTokenDTO), - token = tokenConverter.convert(metadataDTO.tokenDTO), - tokens = metadataDTO.tokensDTO.map { tokenConverter.convert(it) }, - type = metadataDTO.type, - rewardSchedule = convertRewardSchedule(metadataDTO.rewardSchedule), + gasFeeToken = tokenConverter.convert(metadataDTO.gasFeeTokenDTO.asMandatory("gasFeeTokenDTO")), + token = tokenConverter.convert(metadataDTO.tokenDTO.asMandatory("tokenDTO")), + tokens = metadataDTO.tokensDTO.asMandatory("tokensDTO").map { tokenConverter.convert(it) }, + type = metadataDTO.type.asMandatory("type"), + rewardSchedule = convertRewardSchedule(metadataDTO.rewardSchedule.asMandatory("rewardSchedule")), cooldownPeriod = metadataDTO.cooldownPeriod?.let { convertPeriod(it) }, - warmupPeriod = convertPeriod(metadataDTO.warmupPeriod), - rewardClaiming = convertRewardClaiming(metadataDTO.rewardClaiming), + warmupPeriod = convertPeriod(metadataDTO.warmupPeriod.asMandatory("warmupPeriod")), + rewardClaiming = convertRewardClaiming(metadataDTO.rewardClaiming.asMandatory("rewardClaiming")), defaultValidator = metadataDTO.defaultValidator, minimumStake = metadataDTO.minimumStake, - supportsMultipleValidators = metadataDTO.supportsMultipleValidators, - revshare = convertEnabled(metadataDTO.revshare), - fee = convertEnabled(metadataDTO.fee), + supportsMultipleValidators = metadataDTO.supportsMultipleValidators.asMandatory( + "supportsMultipleValidators", + ), + revshare = convertEnabled(metadataDTO.revshare.asMandatory("revshare")), + fee = convertEnabled(metadataDTO.fee.asMandatory("fee")), ) } private fun convertPeriod(periodDTO: YieldDTO.MetadataDTO.PeriodDTO): Yield.Metadata.Period { return Yield.Metadata.Period( - days = periodDTO.days, + days = periodDTO.days.asMandatory("days"), ) } private fun convertEnabled(enabledDTO: YieldDTO.MetadataDTO.EnabledDTO): Yield.Metadata.Enabled { return Yield.Metadata.Enabled( - enabled = enabledDTO.enabled, + enabled = enabledDTO.enabled.asMandatory("enabled"), ) } private fun convertValidator(validatorDTO: YieldDTO.ValidatorDTO): Yield.Validator { + val address = validatorDTO.address.asMandatory("address") return Yield.Validator( - address = validatorDTO.address, - status = convertValidatorStatus(validatorDTO.status), - name = validatorDTO.name, + address = address, + status = convertValidatorStatus(validatorDTO.status.asMandatory("status")), + name = validatorDTO.name.asMandatory("name"), image = validatorDTO.image, website = validatorDTO.website, apr = validatorDTO.apr, commission = validatorDTO.commission, stakedBalance = validatorDTO.stakedBalance, votingPower = validatorDTO.votingPower, - preferred = validatorDTO.preferred, - isStrategicPartner = isStrategicPartner(validatorDTO.address, validatorDTO.name), + preferred = validatorDTO.preferred.asMandatory("preferred"), + isStrategicPartner = isStrategicPartner(validatorDTO.address, validatorDTO.name.asMandatory("name")), ) } @@ -177,7 +180,7 @@ class YieldConverter( } } - private fun isStrategicPartner(validatorAddress: String, validatorName: String): Boolean { + private fun isStrategicPartner(validatorAddress: String?, validatorName: String): Boolean { return PARTNERS.any { it == validatorAddress } || PARTNERS_NAMES.any { it.equals(validatorName, true) } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt index 4bc04836a3..0a4ce1e6ff 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/action/ActionStatusConverter.kt @@ -2,9 +2,10 @@ package com.tangem.data.staking.converters.action import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionStatusDTO import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus -import com.tangem.utils.converter.Converter +import com.tangem.utils.converter.TwoWayConverter + +class ActionStatusConverter : TwoWayConverter { -class ActionStatusConverter : Converter { override fun convert(value: StakingActionStatusDTO): StakingActionStatus { return when (value) { StakingActionStatusDTO.CANCELED -> StakingActionStatus.CANCELED @@ -16,4 +17,16 @@ class ActionStatusConverter : Converter StakingActionStatus.UNKNOWN } } + + override fun convertBack(value: StakingActionStatus): StakingActionStatusDTO { + return when (value) { + StakingActionStatus.CANCELED -> StakingActionStatusDTO.CANCELED + StakingActionStatus.CREATED -> StakingActionStatusDTO.CREATED + StakingActionStatus.WAITING_FOR_NEXT -> StakingActionStatusDTO.WAITING_FOR_NEXT + StakingActionStatus.PROCESSING -> StakingActionStatusDTO.PROCESSING + StakingActionStatus.FAILED -> StakingActionStatusDTO.FAILED + StakingActionStatus.SUCCESS -> StakingActionStatusDTO.SUCCESS + else -> StakingActionStatusDTO.UNKNOWN + } + } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt index d06ab798d2..b46fd75f85 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt @@ -1,7 +1,7 @@ package com.tangem.data.staking.converters.action import com.tangem.data.staking.converters.transaction.StakingTransactionConverter -import com.tangem.datasource.api.stakekit.models.response.EnterActionResponse +import com.tangem.datasource.api.stakekit.models.response.ActionDTO import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.utils.converter.Converter @@ -9,9 +9,9 @@ class EnterActionResponseConverter( private val actionStatusConverter: ActionStatusConverter, private val stakingActionTypeConverter: StakingActionTypeConverter, private val transactionConverter: StakingTransactionConverter, -) : Converter { +) : Converter { - override fun convert(value: EnterActionResponse): StakingAction { + override fun convert(value: ActionDTO): StakingAction { return StakingAction( id = value.id, integrationId = value.integrationId, 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 b9b9bd58d0..f3a7879d5a 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -3,8 +3,8 @@ package com.tangem.data.staking.di import com.squareup.moshi.Moshi import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.common.cache.CacheRegistry +import com.tangem.data.staking.* import com.tangem.data.staking.DefaultStakingErrorResolver -import com.tangem.data.staking.DefaultStakingPendingTransactionRepository import com.tangem.data.staking.DefaultStakingRepository import com.tangem.data.staking.DefaultStakingTransactionHashRepository import com.tangem.data.staking.converters.error.StakeKitErrorConverter @@ -12,12 +12,10 @@ import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.token.StakingActionsStore import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore -import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository -import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.staking.repositories.StakingTransactionHashRepository +import com.tangem.domain.staking.repositories.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles @@ -74,8 +72,14 @@ internal object StakingDataModule { @Provides @Singleton - fun provideStakingPendingTransactionRepository(): StakingPendingTransactionRepository { - return DefaultStakingPendingTransactionRepository() + fun provideStakingActionRepository( + stakingActionsStore: StakingActionsStore, + dispatchers: CoroutineDispatcherProvider, + ): StakingActionRepository { + return DefaultStakingActionRepository( + stakingActionsStore = stakingActionsStore, + dispatchers = dispatchers, + ) } @Provides diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 3e5c70b106..cc7bb7a3fe 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -95,8 +95,14 @@ internal object TokensDataModule { @Provides @Singleton - fun provideCurrencyChecksRepository(walletManagersFacade: WalletManagersFacade): CurrencyChecksRepository { - return DefaultCurrencyChecksRepository(walletManagersFacade = walletManagersFacade) + fun provideCurrencyChecksRepository( + walletManagersFacade: WalletManagersFacade, + coroutineDispatcherProvider: CoroutineDispatcherProvider, + ): CurrencyChecksRepository { + return DefaultCurrencyChecksRepository( + walletManagersFacade = walletManagersFacade, + coroutineDispatchers = coroutineDispatcherProvider, + ) } @Provides diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index a3b8911411..a739407505 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -1,6 +1,5 @@ package com.tangem.data.tokens.repository -import arrow.core.raise.catch import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.compatibility.getL2CompatibilityTokenComparison import com.tangem.blockchainsdk.utils.toCoinId @@ -27,8 +26,6 @@ import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.core.error.DataError -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -41,7 +38,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch +import kotlinx.coroutines.plus import kotlinx.coroutines.withContext import timber.log.Timber import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency @@ -66,10 +63,6 @@ internal class DefaultCurrenciesRepository( private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() private val customTokensMerger = CustomTokensMerger(tangemTechApi, dispatchers) - private val isMultiCurrencyWalletCurrenciesFetching = MutableStateFlow( - value = emptyMap(), - ) - override suspend fun saveTokens( userWalletId: UserWalletId, currencies: List, @@ -206,19 +199,16 @@ internal class DefaultCurrenciesRepository( } } - override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): LceFlow> { - return lceFlow { - val userWallet = catch({ getUserWallet(userWalletId) }) { - raise(it) - } + override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { + return channelFlow { + val userWallet = getUserWallet(userWalletId) if (userWallet.isMultiCurrency) { - getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId).collect(::send) + getMultiCurrencyWalletCurrenciesUpdates(userWalletId).collect(::send) } else { - val currency = catch({ getSingleCurrencyWalletPrimaryCurrency(userWalletId) }) { - raise(it) - } - send(listOf(currency)) + val currencies = getSingleCurrencyWalletWithCardCurrencies(userWalletId) + + send(currencies) } } } @@ -260,40 +250,14 @@ internal class DefaultCurrenciesRepository( val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) - launch(dispatchers.io) { - getMultiCurrencyWalletCurrencies(userWallet) - .collectLatest(::send) - } + getMultiCurrencyWalletCurrencies(userWallet) + .onEach { send(it) } + .launchIn(scope = this + dispatchers.io) withContext(dispatchers.io) { fetchTokensIfCacheExpired(userWallet, refresh = false) } } - .cancellable() - } - - override fun getMultiCurrencyWalletCurrenciesUpdatesLce( - userWalletId: UserWalletId, - ): LceFlow> = lceFlow { - val userWallet = getUserWallet(userWalletId) - catch({ ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) }) { - raise(it) - } - - launch(dispatchers.io) { - combine( - getMultiCurrencyWalletCurrencies(userWallet), - isMultiCurrencyWalletCurrenciesFetching.map { it.getOrElse(userWallet.walletId) { false } }, - ) { currencies, isFetching -> - send(currencies, isStillLoading = isFetching) - }.collect() - } - - withContext(dispatchers.io) { - catch({ fetchTokensIfCacheExpired(userWallet, refresh = false) }) { - raise(it) - } - } } override suspend fun getMultiCurrencyWalletCurrenciesSync( @@ -385,26 +349,34 @@ internal class DefaultCurrenciesRepository( override fun isTokensGrouped(userWalletId: UserWalletId): Flow { return channelFlow { - ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true) + val userWallet = getUserWallet(userWalletId) - launch(dispatchers.io) { + if (userWallet.isMultiCurrency) { getSavedUserTokensResponse(userWalletId) - .map { it.group == UserTokensResponse.GroupType.NETWORK } - .collect(::send) + .map { response -> response.group == UserTokensResponse.GroupType.NETWORK } + .distinctUntilChanged() + .onEach { isGrouped -> send(isGrouped) } + .launchIn(scope = this + dispatchers.io) + } else { + send(element = false) } - }.cancellable() + } } override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { return channelFlow { - ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true) + val userWallet = getUserWallet(userWalletId) - launch(dispatchers.io) { + if (userWallet.isMultiCurrency) { getSavedUserTokensResponse(userWalletId) - .map { it.sort == UserTokensResponse.SortType.BALANCE } - .collect(::send) + .map { response -> response.sort == UserTokensResponse.SortType.BALANCE } + .distinctUntilChanged() + .onEach { isSorted -> send(isSorted) } + .launchIn(scope = this + dispatchers.io) + } else { + send(element = false) } - }.cancellable() + } } override fun isSendBlockedByPendingTransactions( @@ -545,19 +517,7 @@ internal class DefaultCurrenciesRepository( cacheRegistry.invokeOnExpire( key = getTokensCacheKey(userWallet.walletId), skipCache = refresh, - block = { - isMultiCurrencyWalletCurrenciesFetching.update { - it + (userWallet.walletId to true) - } - - try { - fetchTokens(userWallet) - } finally { - isMultiCurrencyWalletCurrenciesFetching.update { - it - userWallet.walletId - } - } - }, + block = { fetchTokens(userWallet) }, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt index ea56a8adbf..a62d70a3f8 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt @@ -2,6 +2,7 @@ package com.tangem.data.tokens.repository import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider import com.tangem.blockchain.common.FeeResourceAmountProvider +import com.tangem.blockchain.common.MinimumSendAmountProvider import com.tangem.blockchain.common.ReserveAmountProvider import com.tangem.blockchain.common.UtxoAmountLimitProvider import com.tangem.data.tokens.converters.UtxoConverter @@ -11,10 +12,13 @@ import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext import java.math.BigDecimal internal class DefaultCurrencyChecksRepository( private val walletManagersFacade: WalletManagersFacade, + private val coroutineDispatchers: CoroutineDispatcherProvider, ) : CurrencyChecksRepository { override suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? { val manager = walletManagersFacade.getOrCreateWalletManager( @@ -43,6 +47,17 @@ internal class DefaultCurrencyChecksRepository( return if (manager is ReserveAmountProvider) manager.getReserveAmount() else null } + override suspend fun getMinimumSendAmount(userWalletId: UserWalletId, network: Network): BigDecimal? { + return withContext(coroutineDispatchers.io) { + val manager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + network = network, + ) + + if (manager is MinimumSendAmountProvider) manager.getMinimumSendAmount() else null + } + } + override suspend fun getFeeResourceAmount(userWalletId: UserWalletId, network: Network): CurrencyAmount? { val manager = walletManagersFacade.getOrCreateWalletManager( userWalletId = userWalletId, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index 3762b4ddee..26ab1470f2 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -1,6 +1,5 @@ package com.tangem.data.tokens.repository -import arrow.core.raise.catch import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchainsdk.utils.fromNetworkId @@ -15,8 +14,6 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress @@ -28,7 +25,10 @@ import com.tangem.domain.walletmanager.model.UpdateWalletManagerResult import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import timber.log.Timber @Suppress("LongParameterList") @@ -46,42 +46,16 @@ internal class DefaultNetworksRepository( private val responseCurrenciesFactory by lazy { ResponseCryptoCurrenciesFactory() } private val networkStatusFactory by lazy { NetworkStatusFactory() } - private val isNetworkStatusesFetching = MutableStateFlow( - value = emptyMap(), - ) - override fun getNetworkStatusesUpdates( userWalletId: UserWalletId, networks: Set, ): Flow> = channelFlow { - launch(dispatchers.io) { - networksStatusesStore.get(userWalletId) - .collectLatest(::send) - } + networksStatusesStore.get(userWalletId) + .onEach(::send) + .launchIn(scope = this + dispatchers.io) withContext(dispatchers.io) { - fetchNetworksStatusesIfCacheExpired(userWalletId, networks, false) - } - } - .cancellable() - - override fun getNetworkStatusesUpdatesLce( - userWalletId: UserWalletId, - networks: Set, - ): LceFlow> = lceFlow { - launch(dispatchers.io) { - combine( - networksStatusesStore.get(userWalletId), - isNetworkStatusesFetching.map { it.getOrElse(userWalletId) { false } }, - ) { statuses, isFetching -> - send(statuses, isStillLoading = isFetching) - }.collect() - } - - withContext(dispatchers.io) { - catch({ fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false) }) { - raise(it) - } + fetchNetworksStatusesIfCacheExpired(userWalletId, networks, refresh = false) } } @@ -127,83 +101,36 @@ internal class DefaultNetworksRepository( } } - override suspend fun getNetworkAddress( - userWalletId: UserWalletId, - currency: CryptoCurrency, - ): CryptoCurrencyAddress = withContext(dispatchers.io) { - CryptoCurrencyAddress( - cryptoCurrency = currency, - address = walletManagersFacade.getAddresses(userWalletId, currency.network) - .firstOrNull { it.type == AddressType.Default } - ?.value.orEmpty(), - ) - } - - override fun getNetworkAddressFlow( - userWalletId: UserWalletId, - currency: CryptoCurrency, - ): Flow = channelFlow { - launch(dispatchers.io) { - send(getNetworkAddress(userWalletId, currency)) - } - } - - override suspend fun getNetworkAddresses(userWalletId: UserWalletId): List = - withContext(dispatchers.io) { - // Get list of currencies matching [network] - val currencies = getCurrencies(userWalletId) - - // There is no currencies matching given [networks] in [userWalletId] - if (currencies.toList().isEmpty()) return@withContext emptyList() - - currencies.toList().map { currency -> - CryptoCurrencyAddress( - cryptoCurrency = currency, - address = walletManagersFacade.getAddresses(userWalletId, currency.network) - .firstOrNull { it.type == AddressType.Default } - ?.value.orEmpty(), - ) - } - } - - override fun getNetworkAddressesFlow( - userWalletId: UserWalletId, - network: Network, - ): Flow> = channelFlow { - launch(dispatchers.io) { - send(getNetworkAddresses(userWalletId, network)) - } - } - - override fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow> = channelFlow { - launch(dispatchers.io) { - send(getNetworkAddresses(userWalletId)) - } - } - private suspend fun fetchNetworksStatusesIfCacheExpired( userWalletId: UserWalletId, networks: Set, refresh: Boolean, - ) { - val currencies = getCurrencies(userWalletId, networks) - val networksDeferred = networks.mapNotNull { network -> - fetchNetworkStatusIfCacheExpired(userWalletId, network, currencies, refresh) + ) = coroutineScope { + if (refresh) { + val statusesToRefresh = networks.map { NetworkStatus(it, NetworkStatus.Refreshing) } + networksStatusesStore.storeAll(userWalletId, statusesToRefresh) } - if (networksDeferred.isNotEmpty()) { - try { - isNetworkStatusesFetching.update { - it + (userWalletId to true) - } + val currencies = getCurrencies(userWalletId, networks) + val networksDeferred = networks.mapNotNull { network -> + coroutineScope { + val key = getNetworksStatusesCacheKey(userWalletId, network) - networksDeferred.awaitAll() - } finally { - isNetworkStatusesFetching.update { - it - userWalletId + if (refresh || cacheRegistry.isExpired(key)) { + async { + cacheRegistry.invokeOnExpire( + key = key, + skipCache = refresh, + block = { fetchNetworkStatus(userWalletId, network, currencies) }, + ) + } + } else { + null } } } + + networksDeferred.awaitAll() } private suspend fun fetchNetworksPendingTransactions( @@ -222,26 +149,6 @@ internal class DefaultNetworksRepository( } } - private suspend fun fetchNetworkStatusIfCacheExpired( - userWalletId: UserWalletId, - network: Network, - currencies: Sequence, - refresh: Boolean, - ): Deferred? = coroutineScope { - val key = getNetworksStatusesCacheKey(userWalletId, network) - if (refresh || cacheRegistry.isExpired(key)) { - async { - cacheRegistry.invokeOnExpire( - key = key, - skipCache = refresh, - block = { fetchNetworkStatus(userWalletId, network, currencies) }, - ) - } - } else { - null - } - } - private suspend fun fetchNetworkStatus( userWalletId: UserWalletId, network: Network, diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index f627d8a830..84f0b8020d 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -3,6 +3,7 @@ package com.tangem.data.transaction import androidx.core.text.isDigitsOnly import com.tangem.blockchain.blockchains.algorand.AlgorandTransactionExtras import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras +import com.tangem.blockchain.blockchains.casper.CasperTransactionExtras import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.hedera.HederaTransactionExtras @@ -274,6 +275,7 @@ internal class DefaultTransactionRepository( Blockchain.Hedera -> HederaTransactionExtras(memo) Blockchain.Algorand -> AlgorandTransactionExtras(memo) Blockchain.InternetComputer -> memo.toLongOrNull()?.let { ICPTransactionExtras(it) } + Blockchain.Casper -> memo.toLongOrNull()?.let { CasperTransactionExtras(it) } else -> null } } diff --git a/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetSelectedAppCurrencyUseCase.kt b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetSelectedAppCurrencyUseCase.kt index 4bd3a52e99..179973c048 100644 --- a/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetSelectedAppCurrencyUseCase.kt +++ b/domain/app-currency/src/main/kotlin/com/tangem/domain/appcurrency/GetSelectedAppCurrencyUseCase.kt @@ -6,10 +6,7 @@ import arrow.core.right import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.repository.AppCurrencyRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEmpty +import kotlinx.coroutines.flow.* class GetSelectedAppCurrencyUseCase( private val appCurrencyRepository: AppCurrencyRepository, @@ -21,4 +18,8 @@ class GetSelectedAppCurrencyUseCase( .catch { emit(SelectedAppCurrencyError.DataError(it).left()) } .onEmpty { emit(SelectedAppCurrencyError.NoAppCurrencySelected.left()) } } + + suspend fun invokeSync(): Either { + return invoke().firstOrNull() ?: SelectedAppCurrencyError.NoAppCurrencySelected.left() + } } \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt index 6204c49430..65e29bca2b 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceFlow.kt @@ -1,11 +1,11 @@ package com.tangem.domain.core.lce +import arrow.atomic.AtomicBoolean import arrow.core.raise.Raise import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.channels.ProducerScope import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.Flow @@ -34,20 +34,19 @@ class LceFlowScope @PublishedApi internal constructor( private val ifLoading: suspend LceFlowScope.(C?) -> Unit, ) : Raise, CoroutineScope by producerScope { + val isLoading: AtomicBoolean = AtomicBoolean(value = true) + /** - * Sends a error of type [E] within the [ProducerScope] and then closes it for send. - * All subsequent sends will be ignored. + * Sends an error of type [E] within the [ProducerScope] without closing. * - * This method blocks the coroutine until a error is handled by the receiver. - * - * If the [ProducerScope] is already closed for send (e.g. after rising another error), it just raises [r] - * without closing. + * This method blocks the coroutine until an error is handled by the receiver. * * @param r Error to raise. */ override fun raise(r: E): Nothing { + isLoading.set(false) + producerScope.trySendBlocking(r.lceError()) - producerScope.close() raise.raise(r.lceError()) } @@ -66,6 +65,8 @@ class LceFlowScope @PublishedApi internal constructor( * @param isStillLoading A flag indicating whether the content is still loading. */ suspend fun send(content: C, isStillLoading: Boolean = false) { + isLoading.set(isStillLoading) + val value = if (isStillLoading) { ifLoading(content) return @@ -81,13 +82,10 @@ class LceFlowScope @PublishedApi internal constructor( * * This method suspends until the [Lce] instance is handled by the receiver. * - * If the [ProducerScope] is closed for send (e.g. after rising a error), it does nothing. - * * @param value The [Lce] instance to send. */ - @OptIn(DelicateCoroutinesApi::class) suspend fun send(value: Lce) { - if (producerScope.isClosedForSend) return + isLoading.set(value.isLoading()) producerScope.send(value) } diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt index 1484205426..dce3342eb3 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/LceRaise.kt @@ -1,6 +1,8 @@ package com.tangem.domain.core.lce import arrow.atomic.Atomic +import arrow.core.Either +import arrow.core.identity import arrow.core.raise.Raise import arrow.core.raise.RaiseDSL import arrow.core.raise.recover @@ -97,6 +99,12 @@ class LceRaise @PublishedApi internal constructor( is Lce.Content -> content is Lce.Error -> raise(r = this) } + + @RaiseDSL + fun Either.bindEither(): C = fold( + ifLeft = { raise(it) }, + ifRight = ::identity, + ) } /** diff --git a/domain/legacy/src/main/assets/contract_methods.json b/domain/legacy/src/main/assets/contract_methods.json index f7bad0ed28..671f0b7d6b 100644 --- a/domain/legacy/src/main/assets/contract_methods.json +++ b/domain/legacy/src/main/assets/contract_methods.json @@ -141,5 +141,65 @@ "info":"swapTokensForExactETC", "source":"https://etc.blockscout.com/tx/0xd79e03ca6b71529c94d576ff78c55b4371e2ef3ff8f9a47007995e5b9e77f879", "name":"swap" + }, + "0x6ab15071": { + "info": "stakePOL", + "source": "https://etherscan.io/tx/0x490d2f31faa3f2fdb1a9505888761d89aba1dd3dcfeb65f08651d094c1ee5021", + "name": "buyVoucher" + }, + "0xe4457a8a": { + "info": "stakePOL", + "source": "https://etherscan.io/tx/0xb7b67ea0261fe2067e285eb942be6883ef34b3d313160f3ce122cad3e91f95f7", + "name": "buyVoucherPOL" + }, + "0xc83ec04d": { + "info": "stakePOL", + "source": "https://etherscan.io/tx/0x0f7fa7154be5ae9bbe229963e3c0d3eea16b74311c50c4a35f3fcffc9519baed", + "name": "sellVoucher_new" + }, + "0xe570b78b": { + "info": "stakePOL", + "source": "https://etherscan.io/tx/0xde1dda9ff3aed8b1d5feff53f72032cbd106a57c1ae2e7fa80016349e2e68341", + "name": "sellVoucher_newPOL" + }, + "0xe97fddc2": { + "info": "stakePOL", + "source": "https://etherscan.io/tx/0x0478bc1229722c049eed737ef5a56baee6d7bde7ae21afad7a10e029700b378c", + "name": "unstakeClaimTokens_new" + }, + "0x8759c234": { + "info": "stakePOL", + "source": "https://etherscan.io/tx/0x79075778b470358bef4cfc25a74a43e7ae60f1bc292e0bca3ebc163fee8bf0c7", + "name": "unstakeClaimTokens_newPOL" + }, + "0xc7b8981c": { + "info": "stakePOL", + "source": "https://etherscan.io/tx/0xc2df9b41f7b3780a2dd06f0dfff72ecd5d3a5bde3f1d3f0eb926e14ff975e957", + "name": "withdrawRewards" + }, + "0xe0db556b": { + "info": "stakePOL", + "source": "https://etherscan.io/tx/0x96ee64ad31a045982a4776336d852af3c25dc39b6d01bea987aefd460b816773", + "name": "withdrawRewardsPOL" + }, + "0x982ef0a7": { + "info": "stakeBSC", + "source": "https://bscscan.com/tx/0xd5ab62ee525f3dc2821de44c31e278f2b49a0202f4e0279e6d0fced05a034595", + "name": "delegate" + }, + "0x4d99dd16": { + "info": "stakeBSC", + "source": "https://bscscan.com/tx/0x866ba857e96051d7cea271723fccde4113c9909dccb534202a2a87ad4c56cb67", + "name": "undelegate" + }, + "0x59491871": { + "info": "stakeBSC", + "source": "https://bscscan.com/tx/0x69615650be1e8a6d94258483f4e880fc6b4d4d9ae53d967c82d847691d91dd71", + "name": "redelegate" + }, + "0xaad3ec96": { + "info": "stakeBSC", + "source": "https://bscscan.com/tx/0xfcbeac6e0a4ba5768d97d14b5115d08dda92defa8a8deee8bba34e2f0655c52b", + "name": "claim" } } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt index 306c18cf6a..bdfee927b3 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/SdkTransactionTypeConverter.kt @@ -21,19 +21,19 @@ internal class SdkTransactionTypeConverter( TxHistoryItem.TransactionType.Transfer } is TransactionType.TronStakingTransactionType.FreezeBalanceV2Contract -> { - TxHistoryItem.TransactionType.TronStakingTransactionType.Stake + TxHistoryItem.TransactionType.Staking.Stake } is TransactionType.TronStakingTransactionType.UnfreezeBalanceV2Contract -> { - TxHistoryItem.TransactionType.TronStakingTransactionType.Unstake + TxHistoryItem.TransactionType.Staking.Unstake } is TransactionType.TronStakingTransactionType.VoteWitnessContract -> { - TxHistoryItem.TransactionType.TronStakingTransactionType.Vote(value.validatorAddress) + TxHistoryItem.TransactionType.Staking.Vote(value.validatorAddress) } is TransactionType.TronStakingTransactionType.WithdrawBalanceContract -> { - TxHistoryItem.TransactionType.TronStakingTransactionType.ClaimRewards + TxHistoryItem.TransactionType.Staking.ClaimRewards } is TransactionType.TronStakingTransactionType.WithdrawExpireUnfreezeContract -> { - TxHistoryItem.TransactionType.TronStakingTransactionType.Withdraw + TxHistoryItem.TransactionType.Staking.Withdraw } } } @@ -43,6 +43,22 @@ internal class SdkTransactionTypeConverter( "transfer" -> TxHistoryItem.TransactionType.Transfer "approve" -> TxHistoryItem.TransactionType.Approve "swap" -> TxHistoryItem.TransactionType.Swap + "buyVoucher", + "buyVoucherPOL", + "delegate", + -> TxHistoryItem.TransactionType.Staking.Stake + "sellVoucher_new", + "sellVoucher_newPOL", + "undelegate", + -> TxHistoryItem.TransactionType.Staking.Unstake + "unstakeClaimTokens_new", + "unstakeClaimTokens_newPOL", + "claim", + -> TxHistoryItem.TransactionType.Staking.Withdraw + "withdrawRewards", + "withdrawRewardsPOL", + -> TxHistoryItem.TransactionType.Staking.ClaimRewards + "redelegate" -> TxHistoryItem.TransactionType.Staking.Restake null -> TxHistoryItem.TransactionType.UnknownOperation else -> TxHistoryItem.TransactionType.Operation(name = methodName.replaceFirstChar { it.titlecase() }) } diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index 9832ec7010..a0caf393f8 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { api(projects.domain.staking.models) api(projects.domain.core) api(projects.core.analytics) + api(projects.core.utils) implementation(deps.kotlin.serialization) implementation(deps.jodatime) diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/PendingTransaction.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/PendingTransaction.kt deleted file mode 100644 index 408de7dc0d..0000000000 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/PendingTransaction.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.domain.staking.model - -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.Token -import com.tangem.domain.staking.model.stakekit.Yield -import java.math.BigDecimal - -data class PendingTransaction( - val groupId: String?, - val token: Token, - val type: BalanceType, - val amount: BigDecimal, - val rawCurrencyId: String?, - val validator: Yield.Validator?, - val balancesId: Int, -) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/SubmitHashData.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/SubmitHashData.kt index 684264334d..662c27c5c8 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/SubmitHashData.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/SubmitHashData.kt @@ -3,5 +3,4 @@ package com.tangem.domain.staking.model data class SubmitHashData( val transactionHash: String, val transactionId: String, - val pendingTransaction: PendingTransaction, ) \ No newline at end of file diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt index a2a0a6b7fe..9f201fb81e 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/Yield.kt @@ -18,6 +18,11 @@ data class Yield( val isAvailable: Boolean, ) { + val preferredValidators: List + get() = validators.filter { it.preferred } + + fun getCurrentToken(rawCurrencyId: String?) = tokens.firstOrNull { rawCurrencyId == it.coinGeckoId } ?: token + @Serializable data class Status( val enter: Boolean, @@ -130,8 +135,6 @@ data class Yield( APR, // simple rate UNKNOWN, } - - fun getCurrentToken(rawCurrencyId: String?) = tokens.firstOrNull { rawCurrencyId == it.coinGeckoId } ?: token } @Serializable diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index d1f0f3a3f3..ef9946cdb7 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -32,14 +32,6 @@ sealed class YieldBalance { .distinctBy { it.validatorAddress } .size } - - fun getBalancesUniqueId(): Int { - // need to exclude rewards because their amount may change frequently - return balance.items - .filter { it.type != BalanceType.REWARDS } - .map { it.amount.toString() + it.type.toString() + it.groupId } - .hashCode() - } } data object Empty : YieldBalance() diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt index 5a59ba211c..0e2d7abe32 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/action/StakingActionCommonType.kt @@ -1,8 +1,12 @@ package com.tangem.domain.staking.model.stakekit.action -enum class StakingActionCommonType { - ENTER, - EXIT, - PENDING_REWARDS, - PENDING_OTHER, +sealed class StakingActionCommonType { + + data object Enter : StakingActionCommonType() + data object Exit : StakingActionCommonType() + sealed class Pending : StakingActionCommonType() { + data object Restake : Pending() + data object Rewards : Pending() + data object Other : Pending() + } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchActionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchActionsUseCase.kt new file mode 100644 index 0000000000..1e437878e6 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchActionsUseCase.kt @@ -0,0 +1,41 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import com.tangem.domain.staking.model.stakekit.NetworkType +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus +import com.tangem.domain.staking.repositories.StakingActionRepository +import com.tangem.domain.staking.repositories.StakingErrorResolver +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Use case for getting pending actions list. + */ +class FetchActionsUseCase( + private val stakingRepository: StakingRepository, + private val stakingActionRepository: StakingActionRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + networkType: NetworkType, + stakingActionStatus: StakingActionStatus, + ): Either { + return Either + .catch { + val actions = stakingRepository.getActions( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + networkType = networkType, + stakingActionStatus = stakingActionStatus, + ) + + stakingActionRepository.store(userWalletId, cryptoCurrency.id, actions) + } + .mapLeft { stakingErrorResolver.resolve(it) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetActionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetActionsUseCase.kt new file mode 100644 index 0000000000..3190d22711 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetActionsUseCase.kt @@ -0,0 +1,34 @@ +package com.tangem.domain.staking + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.core.utils.EitherFlow +import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.staking.repositories.StakingActionRepository +import com.tangem.domain.staking.repositories.StakingErrorResolver +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map + +/** + * Use case for getting pending actions list. + */ +class GetActionsUseCase( + private val stakingActionRepository: StakingActionRepository, + private val stakingErrorResolver: StakingErrorResolver, +) { + + operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrencyId: CryptoCurrency.ID, + ): EitherFlow> { + return stakingActionRepository.get( + userWalletId = userWalletId, + cryptoCurrencyId = cryptoCurrencyId, + ).map, Either>> { it.right() } + .catch { emit(stakingErrorResolver.resolve(it).left()) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingPendingTransactionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingPendingTransactionsUseCase.kt deleted file mode 100644 index 15557232b0..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetStakingPendingTransactionsUseCase.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.domain.staking - -import arrow.core.Either -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository -import com.tangem.domain.wallets.models.UserWalletId - -/** - * Use case for getting saved staking pending transactions. - */ -class GetStakingPendingTransactionsUseCase( - private val stakingPendingTransactionRepository: StakingPendingTransactionRepository, - private val stakingErrorResolver: StakingErrorResolver, -) { - - operator fun invoke(userWalletId: UserWalletId): Either> { - return Either.catch { - stakingPendingTransactionRepository.getTransactionsWithBalanceItems(userWalletId).map { it.second } - }.mapLeft { - stakingErrorResolver.resolve(it) - } - } -} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt index 2c07ae8ea0..29e7148ee2 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt @@ -1,104 +1,143 @@ package com.tangem.domain.staking import arrow.core.Either -import com.tangem.domain.staking.model.PendingTransaction import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.StakingError +import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository -import com.tangem.domain.wallets.models.UserWalletId -import org.joda.time.DateTime +import com.tangem.utils.extensions.isEqualTo import java.math.BigDecimal import java.util.UUID class InvalidatePendingTransactionsUseCase( - private val stakingPendingTransactionRepository: StakingPendingTransactionRepository, private val stakingErrorResolver: StakingErrorResolver, ) { operator fun invoke( - userWalletId: UserWalletId, balanceItems: List, - balancesId: Int, + processingActions: List, ): Either> { return Either.catch { - val (balancesToDisplay, transactionsToRemove) = mergeRealAndPendingTransactions( - realData = balanceItems, - newBalancesId = balancesId, - pendingData = stakingPendingTransactionRepository.getTransactionsWithBalanceItems(userWalletId), + val balancesToDisplay = mergeBalancesAndProcessingActions( + realBalances = balanceItems, + processingActions = processingActions, ) - - stakingPendingTransactionRepository.removeTransactions(userWalletId, transactionsToRemove.toSet()) - balancesToDisplay }.mapLeft { stakingErrorResolver.resolve(it) } } - private fun mergeRealAndPendingTransactions( - realData: List, - newBalancesId: Int, - pendingData: List>, - ): Pair, List> { - val balances = realData.groupBy { BalanceIdentity(it.groupId, it.type, it.amount, it.date) } - .mapValues { it.value.toMutableList() } - .toMutableMap() + private fun mergeBalancesAndProcessingActions( + realBalances: List, + processingActions: List, + ): List { + val balances = realBalances.toMutableList() - val transactionsToRemove = mutableListOf() - - pendingData.forEach { (pendingTransaction, balanceItem) -> - val key = BalanceIdentity(balanceItem.groupId, balanceItem.type, balanceItem.amount, balanceItem.date) - val oldBalancesId = pendingTransaction.balancesId - - when { - newBalancesId != oldBalancesId -> { - transactionsToRemove.add(pendingTransaction) + processingActions.forEach { action -> + when (action.type) { + StakingActionType.STAKE, StakingActionType.VOTE -> { + addStubStakedPendingTransaction(balances, action) } - balances.containsKey(key) -> { - val removed = balances[key]?.removeIf { !it.isPending } ?: false - if (removed) { - balances[key]?.add(balanceItem) + StakingActionType.VOTE_LOCKED -> { + addStubStakedPendingTransaction(balances, action) + removeLockedBalance(balances, action) + } + StakingActionType.WITHDRAW -> { + modifyBalancesByStatus(balances, action, BalanceType.UNSTAKED) + } + StakingActionType.UNLOCK_LOCKED -> { + modifyBalancesByStatus(balances, action, BalanceType.LOCKED) + } + StakingActionType.RESTAKE -> { + modifyBalancesByStatus(balances, action, BalanceType.STAKED) + } + StakingActionType.UNSTAKE -> { + val isFullUnstake = modifyBalancesByStatus(balances, action, BalanceType.STAKED) + if (!isFullUnstake) { + processPartialUnstake(balances, action) } } else -> { - val groupId = UUID.randomUUID().toString() - val now = DateTime.now() - - balances[BalanceIdentity(groupId, BalanceType.STAKED, pendingTransaction.amount, now)] = - mutableListOf( - BalanceItem( - groupId = groupId, - type = pendingTransaction.type, - amount = pendingTransaction.amount, - rawCurrencyId = pendingTransaction.rawCurrencyId, - validatorAddress = pendingTransaction.validator?.address, - date = null, - pendingActions = emptyList(), - token = pendingTransaction.token, - isPending = true, - ), - ) - - balances.entries.find { - it.key.type == BalanceType.STAKED && - it.key.amount == pendingTransaction.amount && - !it.value.any { it.isPending } - } - ?.key - ?.let { balances.remove(it) } + // intentionally do nothing } } } - return balances.values.flatten() to transactionsToRemove + return balances } - private data class BalanceIdentity( - val groupId: String, - val type: BalanceType, - val amount: BigDecimal, - val date: DateTime?, - ) + private fun removeLockedBalance(balances: MutableList, action: StakingAction) { + val index = findBalanceIndex(balances, action, BalanceType.LOCKED) + + if (index != -1) { + balances.removeAt(index) + } + } + + private fun addStubStakedPendingTransaction(balances: MutableList, action: StakingAction) { + balances.add( + BalanceItem( + groupId = UUID.randomUUID().toString(), + token = balances[0].token, + type = BalanceType.STAKED, + amount = action.amount, + rawCurrencyId = null, + validatorAddress = action.validatorAddress ?: action.validatorAddresses?.get(0) ?: "", + date = null, + pendingActions = emptyList(), + isPending = true, + ), + ) + } + + private fun modifyBalancesByStatus( + balances: MutableList, + action: StakingAction, + type: BalanceType, + ): Boolean { + val index = findBalanceIndex(balances, action, type) + + if (index != -1) { + balances[index] = balances[index].copy(isPending = true) + return true + } + + return false + } + + private fun findBalanceIndex(balances: MutableList, action: StakingAction, type: BalanceType): Int { + return balances.indexOfFirst { + !it.isPending && it.amount isEqualTo action.amount && it.type == type + } + } + + private fun processPartialUnstake(balances: MutableList, action: StakingAction) { + val (index, pendingActionAmount) = findPartialUnstake(balances, action) + + if (index != -1) { + val amount = balances[index].amount + balances[index] = balances[index].copy( + amount = amount - pendingActionAmount, + ) // remnants of real one + + balances.add( + balances[index].copy( + amount = pendingActionAmount, + isPending = true, + ), + ) // pending with amount from action + } + } + + private fun findPartialUnstake(balances: MutableList, action: StakingAction): Pair { + val index = balances.indexOfFirst { + !it.isPending && action.amount < it.amount && + it.type == BalanceType.STAKED && + it.validatorAddress == action.validatorAddress + } + return index to action.amount + } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/SavePendingTransactionUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/SavePendingTransactionUseCase.kt deleted file mode 100644 index ef80e06e64..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/SavePendingTransactionUseCase.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.domain.staking - -import arrow.core.Either -import com.tangem.domain.staking.model.PendingTransaction -import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.domain.staking.repositories.StakingErrorResolver -import com.tangem.domain.staking.repositories.StakingPendingTransactionRepository -import com.tangem.domain.wallets.models.UserWalletId - -/** - * Use case for saving hash that failed to submit during staking confirmation - */ -class SavePendingTransactionUseCase( - private val stakingPendingTransactionRepository: StakingPendingTransactionRepository, - private val stakingErrorResolver: StakingErrorResolver, -) { - - operator fun invoke(userWalletId: UserWalletId, transaction: PendingTransaction): Either { - return Either.catch { - stakingPendingTransactionRepository.saveTransaction( - userWalletId = userWalletId, - transaction = transaction, - ) - }.mapLeft { - stakingErrorResolver.resolve(it) - } - } -} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt b/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt index dbe640d0b0..063e0e9f70 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt @@ -136,6 +136,15 @@ sealed class StakingAnalyticsEvent( }, ) + data class DomainError( + val stakeKitDomainError: StakingError.DomainError, + ) : StakingAnalyticsEvent( + event = "App Errors", + params = buildMap { + addIfValueIsNotNull(AnalyticsParam.ERROR_DESCRIPTION, stakeKitDomainError.message) + }, + ) + fun MutableMap.addIfValueIsNotNull(key: String, value: Any?) { if (value != null) { put(key, value.toString()) diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingActionRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingActionRepository.kt new file mode 100644 index 0000000000..142c9d08f6 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingActionRepository.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.staking.repositories + +import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow + +interface StakingActionRepository { + + suspend fun store(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, actions: List) + + fun get(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID): Flow> +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingPendingTransactionRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingPendingTransactionRepository.kt deleted file mode 100644 index 708b8e1352..0000000000 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingPendingTransactionRepository.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.domain.staking.repositories - -import com.tangem.domain.staking.model.PendingTransaction -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.wallets.models.UserWalletId - -interface StakingPendingTransactionRepository { - - fun getTransactionsWithBalanceItems(userWalletId: UserWalletId): List> - - fun saveTransaction(userWalletId: UserWalletId, transaction: PendingTransaction) - - fun removeTransactions(userWalletId: UserWalletId, transactions: Set) -} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index 6f4ba8fd32..e68adf73a9 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -3,14 +3,15 @@ package com.tangem.domain.staking.repositories import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee -import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo +import com.tangem.domain.staking.model.stakekit.NetworkType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.model.stakekit.action.StakingAction +import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction @@ -34,6 +35,13 @@ interface StakingRepository { fun getStakingAvailability(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): StakingAvailability + suspend fun getActions( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + networkType: NetworkType, + stakingActionStatus: StakingActionStatus, + ): List + suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, @@ -50,16 +58,11 @@ interface StakingRepository { refresh: Boolean = false, ) - fun getMultiYieldBalanceFlow( + fun getMultiYieldBalanceUpdates( userWalletId: UserWalletId, cryptoCurrencies: List, ): Flow - fun getMultiYieldBalanceLce( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): LceFlow - suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, cryptoCurrencies: List, diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt index 08a746b12e..d33b011230 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -21,6 +21,11 @@ data class NetworkStatus( */ sealed class Value + /** + * Represents the state where the network is refreshing. + */ + data object Refreshing : Value() + /** * Represents the state where the network is unreachable. * diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveAnalyticsEvent.kt index f97493ffa2..45d43689a8 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenReceiveAnalyticsEvent.kt @@ -1,13 +1,17 @@ package com.tangem.domain.tokens.model.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM sealed class TokenReceiveAnalyticsEvent( event: String, params: Map = mapOf(), ) : AnalyticsEvent("Token / Receive", event, params, null) { - object ReceiveScreenOpened : TokenReceiveAnalyticsEvent(event = "Receive Screen Opened") + class ReceiveScreenOpened(token: String) : TokenReceiveAnalyticsEvent( + event = "Receive Screen Opened", + params = mapOf(TOKEN_PARAM to token), + ) class ButtonCopyAddress(token: String) : TokenReceiveAnalyticsEvent( event = "Button - Copy Address", diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index bea218c179..c661b761bd 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -18,11 +18,6 @@ sealed class TokenScreenAnalyticsEvent( params = mapOf("Token" to token), ) - class Refreshed(token: String) : TokenScreenAnalyticsEvent( - event = "Refreshed", - params = mapOf("Token" to token), - ) - class ButtonRemoveToken(token: String) : TokenScreenAnalyticsEvent( "Button - Remove Token", params = mapOf("Token" to token), diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt index 780c6c48fe..dae8238b02 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt @@ -6,6 +6,7 @@ import java.math.BigDecimal data class CryptoCurrencyCheck( val dustValue: BigDecimal?, val reserveAmount: BigDecimal?, + val minimumSendAmount: BigDecimal?, val existentialDeposit: BigDecimal?, val utxoAmountLimit: UtxoAmountLimit?, val isAccountFunded: Boolean, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt index 394cd7c1dc..f1ef16bca8 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchTokenListUseCase.kt @@ -40,38 +40,39 @@ class FetchTokenListUseCase( * network statuses, and quotes for associated tokens. * * @param userWalletId The ID of the user's wallet. - * @param refresh Indicates whether to force a refresh of the token list data. + * @param mode The refresh mode to control the fetching process. * @return An [Either] representing success (Right) or an error (Left) in fetching the token list. */ - suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either { - return either { - val currencies = fetchCurrencies(userWalletId, refresh) + suspend operator fun invoke( + userWalletId: UserWalletId, + mode: RefreshMode = RefreshMode.NONE, + ): Either = either { + val currencies = fetchCurrencies(userWalletId, refresh = mode.refreshCurrencies) - coroutineScope { - val fetchStatuses = async { - fetchNetworksStatuses( - userWalletId, - currencies.mapTo(hashSetOf()) { it.network }, - refresh, - ) - } - val fetchQuotes = async { - fetchQuotes( - currencies.mapTo(hashSetOf()) { it.id }, - refresh, - ) - } - - val yieldBalances = async { - fetchYieldBalances( - userWalletId = userWalletId, - currencies = currencies, - refresh = refresh, - ) - } - - awaitAll(fetchStatuses, fetchQuotes, yieldBalances) + coroutineScope { + val fetchStatuses = async { + fetchNetworksStatuses( + userWalletId, + currencies.mapTo(hashSetOf()) { it.network }, + refresh = mode.refreshNetworksStatuses, + ) } + val fetchQuotes = async { + fetchQuotes( + currencies.mapTo(hashSetOf()) { it.id }, + refresh = mode.refreshQuotes, + ) + } + + val yieldBalances = async { + fetchYieldBalances( + userWalletId = userWalletId, + currencies = currencies, + refresh = mode.refreshYieldBalances, + ) + } + + awaitAll(fetchStatuses, fetchQuotes, yieldBalances) } } @@ -120,4 +121,33 @@ class FetchTokenListUseCase( catch = { /* Ignore error */ }, ) } + + /** + * Represents the refresh modes available for fetching token list information. + */ + enum class RefreshMode( + internal val refreshCurrencies: Boolean, + internal val refreshNetworksStatuses: Boolean, + internal val refreshQuotes: Boolean, + internal val refreshYieldBalances: Boolean, + ) { + NONE( + refreshCurrencies = false, + refreshNetworksStatuses = false, + refreshQuotes = false, + refreshYieldBalances = false, + ), + FULL( + refreshCurrencies = true, + refreshNetworksStatuses = true, + refreshQuotes = true, + refreshYieldBalances = true, + ), + SKIP_CURRENCIES( + refreshCurrencies = false, + refreshNetworksStatuses = true, + refreshQuotes = true, + refreshYieldBalances = true, + ), + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index c1ac067bff..ad321616d3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -12,7 +12,6 @@ import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.markets.MarketsFeatureToggles import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.isNullOrZero @@ -34,7 +33,6 @@ class GetCryptoCurrencyActionsUseCase( private val networksRepository: NetworksRepository, private val stakingRepository: StakingRepository, private val stakingFeatureToggles: StakingFeatureToggles, - private val marketsFeatureToggles: MarketsFeatureToggles, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -116,7 +114,7 @@ class GetCryptoCurrencyActionsUseCase( // markets // not a custom token - if (marketsFeatureToggles.isFeatureEnabled && cryptoCurrencyStatus.currency.id.rawCurrencyId != null) { + if (cryptoCurrencyStatus.currency.id.rawCurrencyId != null) { activeList.add(TokenActionsState.ActionState.Analytics(ScenarioUnavailabilityReason.None)) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt index aa488b2671..ba9e837248 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt @@ -24,6 +24,7 @@ class GetCurrencyCheckUseCase( val network = currencyStatus.currency.network val dustValue = currencyChecksRepository.getDustValue(userWalletId, network) val reserveAmount = currencyChecksRepository.getReserveAmount(userWalletId, network) + val minimumSendAmount = currencyChecksRepository.getMinimumSendAmount(userWalletId, network) val existentialDeposit = currencyChecksRepository.getExistentialDeposit(userWalletId, network) val isAccountFunded = recipientAddress?.let { currencyChecksRepository.checkIfAccountFunded( @@ -46,6 +47,7 @@ class GetCurrencyCheckUseCase( CryptoCurrencyCheck( dustValue = dustValue, reserveAmount = reserveAmount, + minimumSendAmount = minimumSendAmount, existentialDeposit = existentialDeposit, utxoAmountLimit = utxoAmountLimit, isAccountFunded = isAccountFunded, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt new file mode 100644 index 0000000000..388b88d7e7 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetMinimumTransactionAmountSyncUseCase.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.repository.CurrencyChecksRepository +import com.tangem.domain.wallets.models.UserWalletId +import java.math.BigDecimal + +class GetMinimumTransactionAmountSyncUseCase( + private val currencyChecksRepository: CurrencyChecksRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ): Either = either { + val cryptoCurrency = cryptoCurrencyStatus.currency + currencyChecksRepository.getMinimumSendAmount(userWalletId, cryptoCurrency.network) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNodlTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNodlTokenListUseCase.kt deleted file mode 100644 index abbe7fa327..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetNodlTokenListUseCase.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.left -import com.tangem.domain.core.utils.EitherFlow -import com.tangem.domain.staking.repositories.StakingRepository -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.error.mapper.mapToTokenListError -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.TokenList -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.tokens.operations.TokenListOperations -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository -import com.tangem.domain.tokens.repository.QuotesRepository -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.transformLatest - -/** - * Use case for getting a list of tokens for NODL card - * - * @property currenciesRepository currencies repository - * @property quotesRepository quotes repository - * @property networksRepository networks repository - * @property stakingRepository staking repository - */ -class GetNodlTokenListUseCase( - private val currenciesRepository: CurrenciesRepository, - private val quotesRepository: QuotesRepository, - private val networksRepository: NetworksRepository, - private val stakingRepository: StakingRepository, -) { - - @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(userWalletId: UserWalletId): EitherFlow { - return getTokensStatuses(userWalletId).transformLatest { maybeTokens -> - maybeTokens.fold( - ifLeft = { error -> - emit(error.left()) - }, - ifRight = { tokens -> - emitAll(createTokenList(userWalletId, tokens)) - }, - ) - } - } - - private fun getTokensStatuses(userWalletId: UserWalletId): EitherFlow> { - val operations = CurrenciesStatusesOperations( - userWalletId = userWalletId, - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - stakingRepository = stakingRepository, - ) - - return operations.getCardCurrenciesStatusesFlow() - .map { maybeCurrenciesStatuses -> - maybeCurrenciesStatuses.mapLeft(CurrenciesStatusesOperations.Error::mapToTokenListError) - } - } - - private fun createTokenList( - userWalletId: UserWalletId, - tokens: List, - ): EitherFlow { - val operations = TokenListOperations( - userWalletId = userWalletId, - tokens = tokens, - currenciesRepository = currenciesRepository, - ) - - return operations.getTokenListForSingleCurrencyFlow().map { maybeTokenList -> - maybeTokenList.mapLeft(TokenListOperations.Error::mapToTokenListError) - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt index 012dd2847a..ebf6a72cd7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt @@ -85,9 +85,6 @@ class GetWalletTotalBalanceUseCase( stakingRepository = stakingRepository, ) - return operations.getCurrenciesStatuses( - userWalletId = userWalletId, - isSingleCurrencyWalletsAllowed = true, - ) + return operations.getCurrenciesStatuses(userWalletId) } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyStatusError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyStatusError.kt index c1f5dfaac8..624325cda9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyStatusError.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/CurrencyStatusError.kt @@ -2,7 +2,7 @@ package com.tangem.domain.tokens.error sealed class CurrencyStatusError { - object UnableToCreateCurrency : CurrencyStatusError() + data object UnableToCreateCurrency : CurrencyStatusError() data class DataError(val cause: Throwable) : CurrencyStatusError() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt index 88c7af38bd..2bd9431825 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt @@ -42,7 +42,7 @@ sealed class TokenList { ) : TokenList() /** Represents a state where the token list is empty. */ - object Empty : TokenList() { + data object Empty : TokenList() { override val totalFiatBalance: TotalFiatBalance = TotalFiatBalance.Loaded( amount = BigDecimal.ZERO, @@ -54,4 +54,13 @@ sealed class TokenList { enum class SortType { NONE, BALANCE, } + + /** Get flatten list of cryptocurrency status [CryptoCurrencyStatus] */ + fun flattenCurrencies(): List { + return when (this) { + is GroupedByNetwork -> groups.flatMap(NetworkGroup::currencies) + is Ungrouped -> currencies + is Empty -> emptyList() + } + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt index 419842f017..f56873489c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -1,12 +1,13 @@ package com.tangem.domain.tokens.operations import arrow.core.* +import arrow.core.raise.ensureNotNull import arrow.core.raise.recover import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.lce.lce -import com.tangem.domain.core.utils.lceError -import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.core.lce.lceFlow +import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.staking.repositories.StakingRepository @@ -26,147 +27,125 @@ internal class CurrenciesStatusesLceOperations( private val stakingRepository: StakingRepository, ) { - fun getCurrenciesStatuses( - userWalletId: UserWalletId, - isSingleCurrencyWalletsAllowed: Boolean = false, - ): LceFlow> { + fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow> { return transformToCurrenciesStatuses( userWalletId = userWalletId, - flow = if (isSingleCurrencyWalletsAllowed) { - getWalletCurrencies(userWalletId) - } else { - getMultiCurrencyWalletCurrencies(userWalletId) - }, + currenciesFlow = getWalletCurrencies(userWalletId), ) } @OptIn(ExperimentalCoroutinesApi::class) private fun transformToCurrenciesStatuses( userWalletId: UserWalletId, - flow: LceFlow>, - ): LceFlow> { - return flow.transformLatest transform@{ maybeCurrencies -> - val nonEmptyCurrencies = maybeCurrencies.fold( - ifLoading = { maybeContent -> - emit(createLoadingCurrenciesStatuses(maybeContent)) - return@transform - }, - ifContent = { content -> - val nonEmptyCurrencies = content.toNonEmptyListOrNull() + currenciesFlow: EitherFlow>, + ): LceFlow> = lceFlow { + currenciesFlow.collectLatest { maybeCurrencies -> + val nonEmptyCurrencies = maybeCurrencies.bind().toNonEmptyListOrNull() + ensureNotNull(nonEmptyCurrencies) { TokenListError.EmptyTokens } - if (nonEmptyCurrencies == null) { - emit(TokenListError.EmptyTokens.lceError()) - return@transform - } else { - nonEmptyCurrencies - } - }, - ifError = { error -> - emit(error.lceError()) - return@transform - }, - ) + // This is only 'true' when the flow here is empty, such as during initial loading + if (isLoading.get()) { + val loadingCurrencies = createCurrenciesStatuses( + currencies = nonEmptyCurrencies, + maybeNetworkStatuses = null, + maybeQuotes = null, + maybeYieldBalances = null, + + ) + send(loadingCurrencies) + } val (networks, currenciesIds) = getIds(nonEmptyCurrencies) + fun createCurrenciesStatuses( + maybeQuotes: Either>?, + maybeNetworkStatuses: Either>?, + maybeYieldBalances: Either?, + ): Lce> = createCurrenciesStatuses( + currencies = nonEmptyCurrencies, + maybeQuotes = maybeQuotes, + maybeNetworkStatuses = maybeNetworkStatuses, + maybeYieldBalances = maybeYieldBalances, + ) + combine( getQuotes(currenciesIds), getNetworksStatuses(userWalletId, networks), getYieldBalances(userWalletId, nonEmptyCurrencies), - - ) { maybeQuotes, maybeNetworksStatuses, maybeYieldBalances -> - val statuses = createCurrenciesStatuses( - currencies = nonEmptyCurrencies, - maybeQuotes = maybeQuotes, - maybeNetworkStatuses = maybeNetworksStatuses, - maybeYieldBalances = maybeYieldBalances, - ) - emit(statuses) - }.collect() - } - } - - private fun createLoadingCurrenciesStatuses( - maybeCurrencies: List?, - ): Lce> { - val nonEmptyCurrencies = maybeCurrencies?.toNonEmptyListOrNull() - - val statuses = if (nonEmptyCurrencies == null) { - lceLoading() - } else { - createCurrenciesStatuses( - currencies = nonEmptyCurrencies, - maybeNetworkStatuses = null, - maybeQuotes = null, - maybeYieldBalances = null, + ::createCurrenciesStatuses, ) + .distinctUntilChanged() + .collectLatest { maybeCurrenciesStatuses -> + send(maybeCurrenciesStatuses) + } } - - return statuses } - private fun getWalletCurrencies(userWalletId: UserWalletId): LceFlow> { + private fun getWalletCurrencies(userWalletId: UserWalletId): EitherFlow> { return currenciesRepository.getWalletCurrenciesUpdates(userWalletId) - .map { maybeCurrencies -> - maybeCurrencies.mapError { TokenListError.DataError(it) } - } - } - - private fun getMultiCurrencyWalletCurrencies( - userWalletId: UserWalletId, - ): LceFlow> { - return currenciesRepository.getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId) + .map, Either>> { it.right() } + .catch { emit(TokenListError.DataError(it).left()) } .distinctUntilChanged() - .map { maybeCurrencies -> - maybeCurrencies.mapError { TokenListError.DataError(it) } - } } private fun createCurrenciesStatuses( currencies: NonEmptyList, maybeQuotes: Either>?, - maybeNetworkStatuses: Lce>?, - maybeYieldBalances: Lce?, + maybeNetworkStatuses: Either>?, + maybeYieldBalances: Either?, ): Lce> = lce { - isLoading.set(maybeNetworkStatuses == null) + isLoading.set(maybeNetworkStatuses == null || maybeYieldBalances == null) var quotesRetrievingFailed = false - val networksStatuses = maybeNetworkStatuses?.bindOrNull()?.toNonEmptySetOrNull() + val networksStatuses = maybeNetworkStatuses?.bindEither()?.toNonEmptySetOrNull() + val yieldBalances = maybeYieldBalances?.bindEither() val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) { - quotesRetrievingFailed = true - null - }?.ifEmpty { - quotesRetrievingFailed = true null } - val yieldBalances = maybeYieldBalances?.getOrNull() + if (quotes == null) { + quotesRetrievingFailed = true + } currencies.map { currency -> val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val address = extractAddress(networkStatus) - val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id) - val yieldBalance = if (supportedIntegration.isNullOrBlank().not()) { - (yieldBalances as? YieldBalanceList.Data)?.getBalance( - address = address, - integrationId = supportedIntegration, - ) - } else { - null - } + val yieldBalance = findYieldBalanceOrNull(yieldBalances, currency, networkStatus) - createCurrencyStatus( + val currencyStatus = createCurrencyStatus( currency = currency, quote = quote, networkStatus = networkStatus, yieldBalance = yieldBalance, ignoreQuote = quotesRetrievingFailed, ) + + if (currencyStatus.value is CryptoCurrencyStatus.Loading) { + isLoading.set(true) + } + + currencyStatus } } + private fun findYieldBalanceOrNull( + yieldBalances: YieldBalanceList?, + currency: CryptoCurrency, + networkStatus: NetworkStatus?, + ): YieldBalance? { + if (yieldBalances !is YieldBalanceList.Data) return null + + val supportedIntegration = stakingRepository.getSupportedIntegrationId(currency.id) + + if (supportedIntegration.isNullOrBlank()) return null + + return yieldBalances.getBalance( + address = extractAddress(networkStatus), + integrationId = supportedIntegration, + ) + } + private fun createCurrencyStatus( currency: CryptoCurrency, quote: Quote?, @@ -189,28 +168,27 @@ internal class CurrenciesStatusesLceOperations( return quotesRepository.getQuotesUpdates(tokensIds) .map, Either>> { it.right() } .catch { emit(TokenListError.DataError(it).left()) } + .distinctUntilChanged() } private fun getNetworksStatuses( userWalletId: UserWalletId, networks: NonEmptySet, - ): LceFlow> { - return networksRepository.getNetworkStatusesUpdatesLce(userWalletId, networks) - .map { maybeStatuses -> - maybeStatuses.mapError { TokenListError.DataError(it) } - } + ): EitherFlow> { + return networksRepository.getNetworkStatusesUpdates(userWalletId, networks) + .map, Either>> { it.right() } + .catch { emit(TokenListError.DataError(it).left()) } + .distinctUntilChanged() } private fun getYieldBalances( userWalletId: UserWalletId, cryptoCurrencies: List, - ): LceFlow { - return stakingRepository.getMultiYieldBalanceLce( - userWalletId = userWalletId, - cryptoCurrencies = cryptoCurrencies, - ).map { maybeBalances -> - maybeBalances.mapError { TokenListError.DataError(it) } - } + ): EitherFlow { + return stakingRepository.getMultiYieldBalanceUpdates(userWalletId, cryptoCurrencies) + .map> { it.right() } + .catch { emit(TokenListError.DataError(it).left()) } + .distinctUntilChanged() } private fun getIds(currencies: List): Pair, NonEmptySet> { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt index 34ac4153c2..639ccd6991 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesOperations.kt @@ -120,50 +120,6 @@ internal class CurrenciesStatusesOperations( return createCurrencyStatus(currency, quotes, networkStatus, yieldBalances) } - fun getCardCurrenciesStatusesFlow(): Flow>> { - return flow { - val nonEmptyCurrencies = recover( - block = { getCurrenciesFromCard(userWalletId) }, - recover = { - emit(it.left()) - return@flow - }, - ).toNonEmptyListOrNull() - - if (nonEmptyCurrencies == null) { - val emptyCurrenciesStatuses = emptyList() - - emit(emptyCurrenciesStatuses.right()) - return@flow - } - - val maybeLoadingCurrenciesStatuses = createCurrenciesStatuses( - currencies = nonEmptyCurrencies, - maybeNetworkStatuses = null, - maybeQuotes = null, - maybeYieldBalances = null, - ) - - emit(maybeLoadingCurrenciesStatuses) - - val (networks, currenciesIds) = getIds(nonEmptyCurrencies) - - val currenciesFlow = combine( - getQuotes(currenciesIds), - getNetworksStatuses(networks), - ) { maybeQuotes, maybeNetworksStatuses -> - createCurrenciesStatuses( - currencies = nonEmptyCurrencies, - maybeQuotes = maybeQuotes, - maybeNetworkStatuses = maybeNetworksStatuses, - maybeYieldBalances = null, - ) - } - - emitAll(currenciesFlow) - } - } - suspend fun getCurrencyStatusFlow(currencyId: CryptoCurrency.ID): Flow> { val currency = recover( block = { getMultiCurrencyWalletCurrency(currencyId) }, @@ -377,12 +333,6 @@ internal class CurrenciesStatusesOperations( ) } - private suspend fun Raise.getCurrenciesFromCard(userWalletId: UserWalletId): List { - return catch({ currenciesRepository.getSingleCurrencyWalletWithCardCurrencies(userWalletId) }) { - raise(Error.DataError(it)) - } - } - private fun getQuotes(tokensIds: NonEmptySet): Flow>> { return quotesRepository.getQuotesUpdates(tokensIds) .map, Either>> { quotes -> diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index 6cbaf22b37..ff31ccc64a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -16,7 +16,9 @@ internal class CurrencyStatusOperations( private fun createStatus(): CryptoCurrencyStatus.Value { return when (val status = networkStatus?.value) { - null -> CryptoCurrencyStatus.Loading + null, + is NetworkStatus.Refreshing, + -> CryptoCurrencyStatus.Loading is NetworkStatus.MissedDerivation -> createMissedDerivationStatus() is NetworkStatus.Unreachable -> createUnreachableStatus(status) is NetworkStatus.NoAccount -> createNoAccountStatus(status) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt index 0af48b7898..8f2404de68 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt @@ -29,16 +29,6 @@ internal class TokenListOperations( } } - fun getTokenListForSingleCurrencyFlow(): Flow> { - return flow { - emit( - either { - createTokenList() - }, - ) - } - } - private fun Raise.createTokenList(isGrouped: Boolean, isSortedByBalance: Boolean): TokenList { val nonEmptyCurrencies = tokens.toNonEmptyListOrNull() ?: return TokenList.Empty @@ -54,22 +44,6 @@ internal class TokenListOperations( ) } - private fun Raise.createTokenList(): TokenList { - val nonEmptyCurrencies = tokens.toNonEmptyListOrNull() - ?: return TokenList.Empty - - val isAnyTokenLoading = nonEmptyCurrencies.any { it.value is CryptoCurrencyStatus.Loading } - val fiatBalanceOperations = TokenListFiatBalanceOperations(nonEmptyCurrencies, isAnyTokenLoading) - - return createTokenList( - currencies = nonEmptyCurrencies, - fiatBalance = fiatBalanceOperations.calculateFiatBalance(), - isAnyTokenLoading = isAnyTokenLoading, - isGrouped = false, - isSortedByBalance = false, - ) - } - private fun Raise.createTokenList( currencies: NonEmptyList, fiatBalance: TotalFiatBalance, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt index 7e4bc12ae2..6e27d5c2d7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListSortingOperations.kt @@ -23,11 +23,7 @@ internal class TokenListSortingOperations( sortByBalance: Boolean = tokenList.sortedBy == TokenList.SortType.BALANCE, isAnyTokenLoading: Boolean = tokenList.totalFiatBalance is TotalFiatBalance.Loading, ) : this( - currencies = when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies } - is TokenList.Ungrouped -> tokenList.currencies - is TokenList.Empty -> emptyList() - }, + currencies = tokenList.flattenCurrencies(), isAnyTokenLoading = isAnyTokenLoading, sortByBalance = sortByBalance, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index d8c8c7cf37..bda0e8fdbd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -1,7 +1,6 @@ package com.tangem.domain.tokens.repository import com.tangem.domain.core.error.DataError -import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency @@ -81,7 +80,7 @@ interface CurrenciesRepository { * @param userWalletId The unique identifier of the user wallet. * @return A list of [CryptoCurrency]. */ - fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): LceFlow> + fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> /** * Retrieves the primary cryptocurrency for a specific single-currency user wallet. @@ -130,17 +129,6 @@ interface CurrenciesRepository { */ fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> - /** - * Retrieves updates of the list of cryptocurrencies within a multi-currency wallet. - * - * Loads remote cryptocurrencies if they have expired. - * - * @param userWalletId The unique identifier of the user wallet. - * @return A [LceFlow] emitting the set of cryptocurrencies associated with the user wallet. May emit an - * [DataError.UserWalletError.WrongUserWallet] if single-currency user wallet ID provided. - */ - fun getMultiCurrencyWalletCurrenciesUpdatesLce(userWalletId: UserWalletId): LceFlow> - /** * Retrieves the list of cryptocurrencies within a multi-currency wallet. * diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt index 0b5745a983..4ff37c6420 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrencyChecksRepository.kt @@ -20,6 +20,9 @@ interface CurrencyChecksRepository { /** Returns reserve amount which is required to create an account */ suspend fun getReserveAmount(userWalletId: UserWalletId, network: Network): BigDecimal? + /** Returns minimum send transaction amount */ + suspend fun getMinimumSendAmount(userWalletId: UserWalletId, network: Network): BigDecimal? + /** Returns a fee resource amount available and max for paying fees in several blockchains */ suspend fun getFeeResourceAmount(userWalletId: UserWalletId, network: Network): CurrencyAmount? diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt index a2a99ddea2..2e568b83b4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/NetworksRepository.kt @@ -1,7 +1,5 @@ package com.tangem.domain.tokens.repository -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus @@ -23,20 +21,6 @@ interface NetworksRepository { */ fun getNetworkStatusesUpdates(userWalletId: UserWalletId, networks: Set): Flow> - /** - * Retrieves updates of network statuses of specified blockchain networks for a specific user wallet. - * - * Loads remote network statuses if they have expired. - * - * @param userWalletId The unique identifier of the user wallet. - * @param networks A set of network which statuses are to be retrieved. - * @return A [LceFlow] emitting a set of [NetworkStatus] objects corresponding to the specified networks. - */ - fun getNetworkStatusesUpdatesLce( - userWalletId: UserWalletId, - networks: Set, - ): LceFlow> - /** * Fetches pending transactions for given network * @@ -63,33 +47,8 @@ interface NetworksRepository { fun isNeedToCreateAccountWithoutReserve(network: Network): Boolean - /** - * Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId] - */ - fun getNetworkAddressesFlow(userWalletId: UserWalletId, network: Network): Flow> - /** * Returns list of addresses and crypto currency info of added currencies of [network] in selected wallet [userWalletId] */ suspend fun getNetworkAddresses(userWalletId: UserWalletId, network: Network): List - - /** - * Returns address of [cryptoCurrency] in selected wallet [userWalletId] - */ - suspend fun getNetworkAddress(userWalletId: UserWalletId, currency: CryptoCurrency): CryptoCurrencyAddress - - /** - * Returns address of [cryptoCurrency] in selected wallet [userWalletId] - */ - fun getNetworkAddressFlow(userWalletId: UserWalletId, currency: CryptoCurrency): Flow - - /** - * Returns list of addresses and crypto currency info in selected wallet [userWalletId] - */ - fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow> - - /** - * Returns list of addresses and crypto currency info in selected wallet [userWalletId] - */ - suspend fun getNetworkAddresses(userWalletId: UserWalletId): List } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index cc16af1aba..f68979c6f4 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -3,8 +3,6 @@ package com.tangem.domain.tokens.repository import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.utils.toLce import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.FeePaidCurrency @@ -57,7 +55,7 @@ internal class MockCurrenciesRepository( override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) = Unit - override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): LceFlow> { + override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { return emptyFlow() } @@ -91,12 +89,6 @@ internal class MockCurrenciesRepository( return tokens.map { it.getOrElse { e -> throw e } } } - override fun getMultiCurrencyWalletCurrenciesUpdatesLce( - userWalletId: UserWalletId, - ): LceFlow> { - return tokens.map { it.toLce() } - } - override suspend fun getMultiCurrencyWalletCurrency( userWalletId: UserWalletId, id: CryptoCurrency.ID, diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt index 5b5019dfd6..2e56eb6aa0 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockNetworksRepository.kt @@ -3,15 +3,11 @@ package com.tangem.domain.tokens.repository import arrow.core.Either import arrow.core.getOrElse import com.tangem.domain.core.error.DataError -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.utils.toLce -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyAddress import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -26,13 +22,6 @@ internal class MockNetworksRepository( return statuses.map { it.getOrElse { e -> throw e } } } - override fun getNetworkStatusesUpdatesLce( - userWalletId: UserWalletId, - networks: Set, - ): LceFlow> { - return statuses.map { it.toLce() } - } - override suspend fun fetchNetworkPendingTransactions(userWalletId: UserWalletId, networks: Set) { // no-op } @@ -47,37 +36,10 @@ internal class MockNetworksRepository( override fun isNeedToCreateAccountWithoutReserve(network: Network) = false - override fun getNetworkAddressesFlow( - userWalletId: UserWalletId, - network: Network, - ): Flow> = channelFlow { - send(emptyList()) - } - - override fun getNetworkAddressesFlow(userWalletId: UserWalletId): Flow> = channelFlow { - send(emptyList()) - } - override suspend fun getNetworkAddresses( userWalletId: UserWalletId, network: Network, ): List { return emptyList() } - - override suspend fun getNetworkAddresses(userWalletId: UserWalletId): List { - return emptyList() - } - - override suspend fun getNetworkAddress( - userWalletId: UserWalletId, - currency: CryptoCurrency, - ): CryptoCurrencyAddress = CryptoCurrencyAddress(currency, "") - - override fun getNetworkAddressFlow( - userWalletId: UserWalletId, - currency: CryptoCurrency, - ): Flow = channelFlow { - send(CryptoCurrencyAddress(currency, "")) - } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index 32838c0406..fa3e6119d7 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -4,8 +4,6 @@ import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionStatus import com.tangem.blockchain.common.transaction.Fee -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.lce.lceFlow import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo @@ -20,6 +18,7 @@ import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.flow.flowOf import org.joda.time.DateTime import java.math.BigDecimal @@ -118,6 +117,15 @@ class MockStakingRepository : StakingRepository { cryptoCurrency: CryptoCurrency, ): StakingAvailability = StakingAvailability.Unavailable + override suspend fun getActions( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + networkType: NetworkType, + stakingActionStatus: StakingActionStatus, + ): List { + return emptyList() + } + override suspend fun fetchSingleYieldBalance( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, @@ -146,27 +154,10 @@ class MockStakingRepository : StakingRepository { /* no-op */ } - override fun getMultiYieldBalanceFlow( + override fun getMultiYieldBalanceUpdates( userWalletId: UserWalletId, cryptoCurrencies: List, - ): Flow = channelFlow { - send( - YieldBalanceList.Data( - balances = listOf(YieldBalance.Error), - ), - ) - } - - override fun getMultiYieldBalanceLce( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): LceFlow = lceFlow { - send( - YieldBalanceList.Data( - balances = listOf(YieldBalance.Error), - ), - ) - } + ): Flow = flowOf() override suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 3fb6ba5bc1..3d3f3063c6 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -11,6 +11,8 @@ import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.network.ResultChecker import com.tangem.common.core.TangemSdkError import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.simple import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.TapWorkarounds.isStart2Coin import com.tangem.domain.common.TapWorkarounds.isTangemTwins @@ -24,7 +26,6 @@ import com.tangem.domain.transaction.error.SendTransactionError.Companion.USER_C import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.sdk.extensions.localizedDescriptionRes -import com.tangem.utils.toFormattedString class SendTransactionUseCase( private val demoConfig: DemoConfig, @@ -120,7 +121,7 @@ class SendTransactionUseCase( is BlockchainSdkError.WrappedTangemError -> parseWrappedError(error) is BlockchainSdkError.CreateAccountUnderfunded -> { val minAmount = error.minReserve - val minValue = minAmount.value?.toFormattedString(minAmount.decimals).orEmpty() + val minValue = minAmount.value?.format { simple(minAmount.decimals) }.orEmpty() SendTransactionError.CreateAccountUnderfunded(minValue) } else -> { diff --git a/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt index f96905a1ed..61f2278715 100644 --- a/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt +++ b/domain/txhistory/models/src/main/kotlin/com/tangem/domain/txhistory/models/TxHistoryItem.kt @@ -40,12 +40,13 @@ data class TxHistoryItem( data object UnknownOperation : TransactionType data class Operation(val name: String) : TransactionType - sealed interface TronStakingTransactionType : TransactionType { - data class Vote(val validatorAddress: String) : TronStakingTransactionType - data object ClaimRewards : TronStakingTransactionType - data object Stake : TronStakingTransactionType - data object Unstake : TronStakingTransactionType - data object Withdraw : TronStakingTransactionType + sealed interface Staking : TransactionType { + data class Vote(val validatorAddress: String) : Staking + data object ClaimRewards : Staking + data object Stake : Staking + data object Unstake : Staking + data object Withdraw : Staking + data object Restake : Staking } } diff --git a/features/details/api/build.gradle.kts b/features/details/api/build.gradle.kts index ce267fe5d1..72035f72bb 100644 --- a/features/details/api/build.gradle.kts +++ b/features/details/api/build.gradle.kts @@ -16,4 +16,7 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) } \ No newline at end of file diff --git a/features/disclaimer/api/build.gradle.kts b/features/disclaimer/api/build.gradle.kts index 7b97fa25c1..d6fd71c5e8 100644 --- a/features/disclaimer/api/build.gradle.kts +++ b/features/disclaimer/api/build.gradle.kts @@ -13,4 +13,7 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index a0058fc2e9..abecdf97cb 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.update internal class PreviewManageTokensComponent( private val isLoading: Boolean, + private val showTangemIcon: Boolean, params: ManageTokensComponent.Params, ) : ManageTokensComponent { @@ -65,6 +66,7 @@ internal class PreviewManageTokensComponent( loadMore = { false }, saveChanges = {}, isSavingInProgress = false, + needToAddDerivations = showTangemIcon, ), ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt index b4fbaea7be..8f3c97fef8 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/customtoken/CustomTokenFormUM.kt @@ -14,6 +14,7 @@ internal data class CustomTokenFormUM( val notifications: PersistentList = persistentListOf(), val canAddToken: Boolean = false, val isValidating: Boolean = false, + val needToAddDerivation: Boolean = false, val saveToken: () -> Unit, ) { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt index bdec914faa..fbe35f92e4 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/item/CurrencyNetworkUM.kt @@ -14,6 +14,4 @@ internal data class CurrencyNetworkUM( ) : SelectableItemUM { override val id: String = network.id.value - - data class LongTapConfig(val contractAddress: String, val onLongTap: () -> Unit) } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt index d16e17e84a..708b149aca 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt @@ -42,6 +42,7 @@ internal sealed class ManageTokensUM { val saveChanges: () -> Unit, val hasChanges: Boolean, val isSavingInProgress: Boolean, + val needToAddDerivations: Boolean, ) : ManageTokensUM() fun copySealed( @@ -52,6 +53,7 @@ internal sealed class ManageTokensUM { isNextBatchLoading: Boolean = this.isNextBatchLoading, isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress, scrollToTop: StateEvent = this.scrollToTop, + needToAddDerivations: Boolean = this is ManageContent && this.needToAddDerivations, ): ManageTokensUM { return when (this) { is ManageContent -> copy( @@ -62,6 +64,7 @@ internal sealed class ManageTokensUM { isNextBatchLoading = isNextBatchLoading, isSavingInProgress = isSavingInProgress, scrollToTop = scrollToTop, + needToAddDerivations = needToAddDerivations, ) is ReadContent -> copy( search = search, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index a6a587dc8c..b0b7f781d9 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.ContentMessage import com.tangem.domain.card.DerivePublicKeysUseCase +import com.tangem.domain.card.HasMissedDerivationsUseCase import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationException import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.model.CryptoCurrency @@ -43,6 +44,7 @@ internal class CustomTokenFormModel @Inject constructor( private val customCurrencyValidator: CustomCurrencyValidator, private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val derivePublicKeysUseCase: DerivePublicKeysUseCase, + private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, private val messageSender: UiMessageSender, private val customTokenFormManager: CustomCurrencyFormBuilder, private val analyticsEventHandler: AnalyticsEventHandler, @@ -159,7 +161,12 @@ internal class CustomTokenFormModel @Inject constructor( fillForm: Boolean, isAlreadyAdded: Boolean, isCustom: Boolean, - ) { + ) = modelScope.launch { + val needToAddDerivation = hasMissedDerivationsUseCase( + userWalletId = params.userWalletId, + networksWithDerivationPath = mapOf(currency.network.backendId to getDerivationPath().value), + ) + state.update { state -> var updatedState = state .updateWithProgress( @@ -169,6 +176,7 @@ internal class CustomTokenFormModel @Inject constructor( clearNotifications = true, clearFieldErrors = true, disableSecondaryFields = !isCustom, + needToAddDerivation = needToAddDerivation, ) if (fillForm) { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index f5375b9b43..36916e335d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -17,6 +17,7 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.card.HasMissedDerivationsUseCase import com.tangem.domain.managetokens.SaveManagedTokensUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent @@ -47,6 +48,7 @@ internal class ManageTokensModel @Inject constructor( private val router: Router, private val manageTokensListManager: ManageTokensListManager, private val messageSender: UiMessageSender, + private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, private val saveManagedTokensUseCase: SaveManagedTokensUseCase, private val analyticsEventHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, @@ -140,6 +142,7 @@ internal class ManageTokensModel @Inject constructor( hasChanges = false, saveChanges = ::saveChanges, loadMore = ::loadMoreItems, + needToAddDerivations = false, isSavingInProgress = false, ) } @@ -255,10 +258,22 @@ internal class ManageTokensModel @Inject constructor( } private fun updateChangedItems(currenciesToAdd: ChangedCurrencies, currenciesToRemove: ChangedCurrencies) { - state.update { state -> - state.copySealed( - hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(), - ) + modelScope.launch { + val hasMissedDerivations = params.userWalletId?.let { walletId -> + val networks = currenciesToAdd.values + .flatten() + .toSet() + .associate { it.backendId to null } + + hasMissedDerivationsUseCase(walletId, networks) + } + + state.update { state -> + state.copySealed( + hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(), + needToAddDerivations = hasMissedDerivations ?: false, + ) + } } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt index 4c3c730ef6..6b4e801849 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt @@ -61,9 +61,11 @@ internal class OnboardingManageTokensModel @Inject constructor( .onEach { status -> updatePaginationStatus(status) } .launchIn(modelScope) - manageTokensListManager.currenciesToAdd - .onEach(::handleNewAddedCurrencies) - .launchIn(modelScope) + combine( + manageTokensListManager.currenciesToAdd, + manageTokensListManager.currenciesToRemove, + ::handleChangedCurrencies, + ).launchIn(modelScope) observeSearchQueryChanges() @@ -192,8 +194,11 @@ internal class OnboardingManageTokensModel @Inject constructor( state.update { state -> state.copy(scrollToTop = consumedEvent()) } } - private suspend fun handleNewAddedCurrencies(currenciesToAdd: ChangedCurrencies) { - if (currenciesToAdd.isEmpty()) { + private suspend fun handleChangedCurrencies( + currenciesToAdd: ChangedCurrencies, + currenciesToRemove: ChangedCurrencies, + ) { + if (currenciesToAdd.isEmpty() && currenciesToRemove.isEmpty()) { state.update { state -> state.copy( actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Later(onClick = ::onLaterClick), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt index 5f5798395b..fca42a82dc 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenFormContent.kt @@ -22,9 +22,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach -import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.block.information.InformationBlock import com.tangem.core.ui.components.bottomFade +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.fields.SimpleTextField import com.tangem.core.ui.components.isOpened import com.tangem.core.ui.components.keyboardAsState @@ -82,14 +84,22 @@ internal fun CustomTokenFormContent(model: CustomTokenFormUM, modifier: Modifier } } - PrimaryButton( + TangemButton( modifier = Modifier .align(Alignment.BottomCenter) .padding(bottom = TangemTheme.dimens.spacing16 + bottomBarHeight) .fillMaxWidth(), text = stringResource(id = R.string.custom_token_add_token), + colors = TangemButtonsDefaults.primaryButtonColors, enabled = model.canAddToken, showProgress = model.isValidating, + animateContentChange = true, + icon = if (model.needToAddDerivation) { + TangemButtonIconPosition.End(R.drawable.ic_tangem_24) + } else { + TangemButtonIconPosition.None + }, + textStyle = TangemTheme.typography.subtitle1, onClick = model.saveToken, ) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt index 21f3daf5e1..2fed1003f0 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt @@ -285,6 +285,18 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider : onDerivationPathSelected = {}, ), ), + PreviewCustomTokenSelectorComponent( + params = CustomTokenSelectorComponent.Params.NetworkSelector( + userWalletId = UserWalletId(stringValue = "321"), + selectedNetwork = SelectedNetwork( + id = Network.ID(value = "0"), + name = "Ethereum", + derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0"), + canHandleTokens = true, + ), + onNetworkSelected = {}, + ), + ), ) } // endregion Preview \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index 6b2547a8fd..3a85636b3e 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -33,11 +33,17 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed -import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.BottomFade +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.TangemSwitch +import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.SearchBar @@ -122,6 +128,7 @@ internal fun ManageTokensScreen(state: ManageTokensUM, modifier: Modifier = Modi .fillMaxWidth(), isVisible = state.hasChanges, showProgress = state.isSavingInProgress, + showIcon = state.needToAddDerivations, onClick = state.saveChanges, ) } @@ -158,6 +165,7 @@ private fun ManageTokensTopBar(topBar: ManageTokensTopBarUM?, search: SearchBarU private fun SaveChangesButton( isVisible: Boolean, showProgress: Boolean, + showIcon: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -168,10 +176,18 @@ private fun SaveChangesButton( exit = fadeOut(), label = "save_button_visibility", ) { - PrimaryButtonIconEnd( + TangemButton( text = stringResource(id = R.string.common_save), - iconResId = R.drawable.ic_tangem_24, + icon = if (showIcon) { + TangemButtonIconPosition.End(R.drawable.ic_tangem_24) + } else { + TangemButtonIconPosition.None + }, showProgress = showProgress, + colors = TangemButtonsDefaults.primaryButtonColors, + textStyle = TangemTheme.typography.subtitle1, + enabled = true, + animateContentChange = true, onClick = onClick, ) } @@ -472,6 +488,7 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider, private val messageSender: UiMessageSender, private val dispatchers: CoroutineDispatcherProvider, private val scopeProvider: Provider, + private val sourceProvider: Provider, private val actions: ManageTokensUiActions, private val clipboardManager: ClipboardManager, ) { @@ -41,6 +44,9 @@ internal class ManageTokensUiManager( private val scope: CoroutineScope get() = scopeProvider() + private val source: ManageTokensSource + get() = sourceProvider() + @OptIn(ExperimentalCoroutinesApi::class) val items: Flow> = state .mapLatest { state -> @@ -225,26 +231,39 @@ internal class ManageTokensUiManager( } else { actions.checkHasLinkedTokens(userWalletId, network) } + val canHideWithoutConfirming = source == ManageTokensSource.ONBOARDING - val message = ContentMessage { onDismiss -> - if (hasLinkedTokens) { - HasLinkedTokensWarning( - currency = currency, - network = network, - onDismiss = onDismiss, - ) - } else { - HideTokenWarning( - currency = currency, - onConfirm = { - onConfirm() - onDismiss() - }, - onDismiss = onDismiss, - ) - } + if (hasLinkedTokens) { + showLinkedTokensWarning(currency, network) + } else if (canHideWithoutConfirming) { + onConfirm() + } else { + showHideTokenWarning(currency, onConfirm) } + } + private fun showLinkedTokensWarning(currency: ManagedCryptoCurrency, network: Network) { + val message = ContentMessage { onDismiss -> + HasLinkedTokensWarning( + currency = currency, + network = network, + onDismiss = onDismiss, + ) + } + messageSender.send(message) + } + + private fun showHideTokenWarning(currency: ManagedCryptoCurrency, onConfirm: () -> Unit) { + val message = ContentMessage { onDismiss -> + HideTokenWarning( + currency = currency, + onConfirm = { + onConfirm() + onDismiss() + }, + onDismiss = onDismiss, + ) + } messageSender.send(message) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt index 03c990ab76..b2e1f4fbdb 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/ui/CustomCurrencyFormOperations.kt @@ -27,6 +27,7 @@ internal fun CustomTokenFormUM.updateWithProgress( showProgress: Boolean, isWasFilled: Boolean = this.tokenForm?.wasFilled ?: false, canAddToken: Boolean = this.canAddToken, + needToAddDerivation: Boolean = false, clearNotifications: Boolean = false, clearFieldErrors: Boolean = false, disableSecondaryFields: Boolean = false, @@ -34,6 +35,7 @@ internal fun CustomTokenFormUM.updateWithProgress( return copy( isValidating = showProgress, canAddToken = canAddToken, + needToAddDerivation = needToAddDerivation, notifications = if (clearNotifications) persistentListOf() else notifications, ).updateTokenForm { val updatedFields = fields.mapValues { (key, field) -> diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsFeatureToggles.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsFeatureToggles.kt deleted file mode 100644 index 34758a7b41..0000000000 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/MarketsFeatureToggles.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.features.markets - -interface MarketsFeatureToggles { - val isFeatureEnabled: Boolean -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsFeatureToggles.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsFeatureToggles.kt deleted file mode 100644 index 75062eddef..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/DefaultMarketsFeatureToggles.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.features.markets - -import com.tangem.core.featuretoggle.manager.FeatureTogglesManager - -internal class DefaultMarketsFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : MarketsFeatureToggles { - - override val isFeatureEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("MARKETS_ENABLED") -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt index 545104f625..3216b03dd3 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt @@ -44,6 +44,13 @@ internal class MarketDetailsAnalyticsEvent( "Link" to linkTitle, ), ) + + fun exchangesScreenOpened() = MarketDetailsAnalyticsEvent( + event = "Exchanges Screen Opened", + params = mapOf( + "Token" to token.symbol, + ), + ) } enum class IntervalType(val source: String) { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt index ce7de0c619..11ae6093f5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt @@ -17,6 +17,8 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -172,12 +174,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( fiatCurrencySymbol = currentAppCurrency.value.symbol, ), dateTimeText = resourceReference(R.string.common_today), - priceChangePercentText = params.token.tokenQuotes.h24Percent?.let { - BigDecimalFormatter.formatPercent( - percent = it, - useAbsoluteValue = true, - ) - }, + priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() }, priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(), iconUrl = params.token.imageUrl, chartState = MarketsTokenDetailsUM.ChartState( @@ -472,12 +469,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) } ?: currentQuotes.value.getPercentByInterval(currentState.selectedInterval) - val percentText = percent?.let { - BigDecimalFormatter.formatPercent( - percent = it, - useAbsoluteValue = true, - ) - } ?: "" + val percentText = percent?.format { percent() } ?: "" state.update { stateToUpdate -> stateToUpdate.copy( @@ -535,6 +527,8 @@ internal class MarketsTokenDetailsModel @Inject constructor( private fun onListedOnClick(exchangesCount: Int) { modelScope.launch { + analyticsEventHandler.send(analyticsEventBuilder.exchangesScreenOpened()) + showBottomSheet(content = ExchangesBottomSheetContent.Loading(exchangesCount)) // Delay to show the bottom sheet diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt index fb93bd86a2..65273ee0bb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt @@ -3,7 +3,10 @@ package com.tangem.features.markets.details.impl.model.converters import androidx.compose.runtime.Stable import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.compact +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.rawCompact import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenMarketInfo @@ -142,14 +145,17 @@ internal class InsightsConverter( private fun BigDecimal.convertChange(isFiatValue: Boolean = false): String { val value = if (isFiatValue) { - val currency = appCurrency() - BigDecimalFormatter.formatCompactFiatAmount( - amount = this.abs(), - fiatCurrencyCode = currency.code, - fiatCurrencySymbol = currency.symbol, - ) + this.abs().format { + val currency = appCurrency() + fiat( + fiatCurrencyCode = currency.code, + fiatCurrencySymbol = currency.symbol, + ).compact() + } } else { - BigDecimalFormatter.formatCompactAmount(amount = this.abs()) + this.abs().format { + rawCompact() + } } return when { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt index 48b3217b3d..c91039839b 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt @@ -2,7 +2,10 @@ package com.tangem.features.markets.details.impl.model.converters import androidx.compose.runtime.Stable import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.compact +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketInfo import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent @@ -129,18 +132,21 @@ internal class MetricsConverter( if (this == null) return StringsSigns.DASH_SIGN return if (crypto) { - BigDecimalFormatter.formatCompactCryptoAmount( - amount = this, - cryptoCurrencySymbol = tokenSymbol, - ) + format { + crypto( + symbol = tokenSymbol, + decimals = 2, + ).compact() + } } else { val currency = appCurrency() - BigDecimalFormatter.formatCompactFiatAmount( - amount = this, - fiatCurrencyCode = currency.code, - fiatCurrencySymbol = currency.symbol, - ) + format { + fiat( + fiatCurrencyCode = currency.code, + fiatCurrencySymbol = currency.symbol, + ).compact() + } } } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt index 4c185c04e3..890606701f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt @@ -2,6 +2,8 @@ package com.tangem.features.markets.details.impl.model.formatter import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.PriceChangeInterval @@ -28,12 +30,7 @@ internal fun TokenQuotes.getFormattedPercentByInterval(interval: PriceChangeInte PriceChangeInterval.ALL_TIME -> allTimeChangePercent } - return percent?.let { - BigDecimalFormatter.formatPercent( - percent = it, - useAbsoluteValue = true, - ) - } ?: "" + return percent?.format { percent() } ?: "" } internal fun TokenQuotes.getPercentByInterval(interval: PriceChangeInterval): BigDecimal? { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index e0e25052fc..c55ba95bec 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -20,7 +20,6 @@ import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.markets.SaveMarketTokensUseCase import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.markets.impl.R diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt index daaa385754..abd9b2ccdc 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt @@ -8,12 +8,14 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.key +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SmallButtonShimmer import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.block.information.InformationBlock @@ -76,10 +78,26 @@ private fun Title() { private fun AddButton(state: AddButtonState, onClick: () -> Unit) { when (state) { AddButtonState.Loading -> { - SmallButtonShimmer( - modifier = Modifier.size(width = TangemTheme.dimens.size63, height = TangemTheme.dimens.size18), - shape = RoundedCornerShape(TangemTheme.dimens.radius3), - ) + Box { + SmallButtonShimmer( + modifier = Modifier.width(width = TangemTheme.dimens.size63), + shape = RoundedCornerShape(TangemTheme.dimens.radius3), + withIcon = true, + ) + + Box( + Modifier + .matchParentSize() + .background(TangemTheme.colors.background.action), + ) + + RectangleShimmer( + modifier = Modifier + .align(Alignment.Center) + .size(width = TangemTheme.dimens.size63, height = TangemTheme.dimens.size18), + radius = TangemTheme.dimens.radius3, + ) + } } AddButtonState.Available, AddButtonState.Unavailable, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt index d0ca34298a..35346aeb94 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/TokenMarketBlockModel.kt @@ -11,6 +11,8 @@ 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.marketprice.PriceChangeType +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -91,10 +93,7 @@ internal class TokenMarketBlockModel @Inject constructor( // TODO get currency from quotes use case [REDACTED_TASK_KEY] fiatCurrencySymbol = currentAppCurrency.value.symbol, ), - h24Percent = BigDecimalFormatter.formatPercent( - percent = res.priceChange, - useAbsoluteValue = true, - ), + h24Percent = res.priceChange.format { percent() }, priceChangeType = PriceChangeType.fromBigDecimal(res.priceChange), ) } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt index b59acbc16d..41fc02dedd 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/tokenlist/impl/model/converters/MarketsTokenItemConverter.kt @@ -5,6 +5,10 @@ import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter import com.tangem.common.ui.charts.state.sorted import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.format.bigdecimal.compact +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarket @@ -71,12 +75,14 @@ internal class MarketsTokenItemConverter( private fun TokenMarket.getMarketCap(): String? { val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null - return BigDecimalFormatter.formatCompactFiatAmount( - amount = value, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - threeDigitsMethod = true, - ) + return value.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).compact( + threeDigitsMethod = true, + ) + } } private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price { @@ -144,9 +150,6 @@ internal class MarketsTokenItemConverter( TrendInterval.M1 -> tokenQuotesShort.monthChangePercent } - return BigDecimalFormatter.formatPercent( - percent = percent, - useAbsoluteValue = true, - ) + return percent.format { percent() } } } \ No newline at end of file diff --git a/features/onboarding-v2/api/.gitignore b/features/onboarding-v2/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/onboarding-v2/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/onboarding-v2/api/build.gradle.kts b/features/onboarding-v2/api/build.gradle.kts new file mode 100644 index 0000000000..92e55d627f --- /dev/null +++ b/features/onboarding-v2/api/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.onboarding.v2.api" +} + +dependencies { + + /* Project - Domain */ + implementation(projects.domain.models) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt new file mode 100644 index 0000000000..e86227d668 --- /dev/null +++ b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.onboarding.v2 + +interface OnboardingV2FeatureToggles { + val isOnboardingV2Enabled: Boolean +} \ No newline at end of file diff --git a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt new file mode 100644 index 0000000000..847e64541e --- /dev/null +++ b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/entry/OnboardingEntryComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.onboarding.v2.entry + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.scan.ScanResponse + +interface OnboardingEntryComponent : ComposableContentComponent { + + data class Params( + val scanResponse: ScanResponse, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/.gitignore b/features/onboarding-v2/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/onboarding-v2/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts new file mode 100644 index 0000000000..25f02a083b --- /dev/null +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -0,0 +1,61 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.onboarding.v2.impl" +} + +dependencies { + /** Api */ + implementation(projects.features.onboardingV2.api) + + /** Core modules */ + implementation(projects.core.featuretoggles) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + implementation(projects.core.utils) + implementation(projects.core.ui) + implementation(projects.core.res) + implementation(projects.core.decompose) + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.feedback) + implementation(projects.domain.core) + + /** Tangem libraries */ + implementation(projects.libs.tangemSdkApi) + implementation(deps.tangem.card.core) + implementation(deps.tangem.card.android) { + exclude(module = "joda-time") + } + + /** AndroidX libraries */ + implementation(deps.androidx.core.ktx) + implementation(deps.lifecycle.runtime.ktx) + + /** Compose libraries */ + implementation(deps.compose.material) // to use buttons + implementation(deps.compose.material3) + implementation(deps.compose.animation) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.coil) + implementation(deps.decompose.ext.compose) + + /** Other libraries */ + implementation(deps.kotlin.immutable.collections) + implementation(deps.kotlin.serialization) + implementation(deps.timber) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt new file mode 100644 index 0000000000..fe67d84f57 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.features.onboarding.v2 + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import javax.inject.Inject + +internal class DefaultOnboardingV2FeatureToggles @Inject constructor( + private val featureTogglesManager: FeatureTogglesManager, +) : OnboardingV2FeatureToggles { + override val isOnboardingV2Enabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("ONBOARDING_CODE_REFACTORING_ENABLED") +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/TitleProvider.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/TitleProvider.kt new file mode 100644 index 0000000000..58dc49d42d --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/TitleProvider.kt @@ -0,0 +1,9 @@ +package com.tangem.features.onboarding.v2 + +import com.tangem.core.ui.extensions.TextReference +import kotlinx.coroutines.flow.StateFlow + +interface TitleProvider { + val currentTitle: StateFlow + fun changeTitle(text: TextReference) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/di/FeatureModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/di/FeatureModule.kt new file mode 100644 index 0000000000..58f1c6cd79 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/di/FeatureModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.onboarding.v2.di + +import com.tangem.features.onboarding.v2.DefaultOnboardingV2FeatureToggles +import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface FeatureModule { + + @Singleton + @Binds + fun provideFeatureToggles(featureToggles: DefaultOnboardingV2FeatureToggles): OnboardingV2FeatureToggles +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt new file mode 100644 index 0000000000..ac06b7c4a5 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/DefaultOnboardingEntryComponent.kt @@ -0,0 +1,130 @@ +package com.tangem.features.onboarding.v2.entry.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.stack.ChildStack +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.value.Value +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.decompose.navigation.inner.InnerNavigationHolder +import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent +import com.tangem.features.onboarding.v2.entry.impl.model.OnboardingEntryModel +import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingChildFactory +import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute +import com.tangem.features.onboarding.v2.entry.impl.ui.OnboardingEntry +import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +internal class DefaultOnboardingEntryComponent @AssistedInject constructor( + @Assisted val context: AppComponentContext, + @Assisted val params: OnboardingEntryComponent.Params, + stepperFactory: OnboardingStepperComponent.Factory, + onboardingChildFactory: OnboardingChildFactory, +) : OnboardingEntryComponent, AppComponentContext by context { + + private val innerNavigationLinkJobHolder = JobHolder() + private val model: OnboardingEntryModel = getOrCreateModel(params) + + private val stepperComponent = stepperFactory.create( + context = child("stepper"), + params = OnboardingStepperComponent.Params( + scanResponse = params.scanResponse, + initState = OnboardingStepperComponent.StepperState( + currentStep = 0, + steps = 0, + title = model.titleProvider.currentTitle.value, + showProgress = false, + ), + popBack = { + popInternal { success -> + if (success.not()) { + stackNavigation.pop() + } + } + }, + ), + ) + + private val stackNavigation = StackNavigation() + + private val innerStack: Value> = childStack( + key = "innerStack", + source = stackNavigation, + serializer = null, + initialConfiguration = model.state.value.currentRoute, + handleBackButton = true, + childFactory = { configuration, factoryContext -> + onboardingChildFactory.createChild( + route = configuration, + childContext = childByContext(factoryContext), + ) + }, + ) + + init { + linkToInnerNavigation() + } + + private fun popInternal(onComplete: (Boolean) -> Unit) { + val activeChild = innerStack.value.active.instance + if (activeChild is InnerNavigationHolder) { + activeChild.innerNavigation.pop(onComplete) + } else { + onComplete(false) + } + } + + private fun linkToInnerNavigation() { + innerStack.observe { stack -> + val activeChild = stack.active.instance + if (activeChild is InnerNavigationHolder) { + componentScope.launch { + activeChild.innerNavigation.state.collect { state -> + stepperComponent.state.update { + it.copy( + currentStep = state.stackSize, + steps = state.stackMaxSize ?: 0, + title = model.titleProvider.currentTitle.value, + showProgress = state.stackMaxSize != null, + ) + } + } + }.saveIn(innerNavigationLinkJobHolder) + } + } + } + + @Composable + override fun Content(modifier: Modifier) { + val innerStackState by innerStack.subscribeAsState() + + OnboardingEntry( + modifier = modifier, + stepperContent = { modifierParam -> + stepperComponent.Content(modifierParam) + }, + childStack = innerStackState, + ) + } + + @AssistedFactory + interface Factory : OnboardingEntryComponent.Factory { + override fun create( + context: AppComponentContext, + params: OnboardingEntryComponent.Params, + ): DefaultOnboardingEntryComponent + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..1ea00b3585 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/di/ComponentModule.kt @@ -0,0 +1,34 @@ +package com.tangem.features.onboarding.v2.entry.impl.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent +import com.tangem.features.onboarding.v2.entry.impl.DefaultOnboardingEntryComponent +import com.tangem.features.onboarding.v2.entry.impl.model.OnboardingEntryModel +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 +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindOnboardingEntryComponent( + factory: DefaultOnboardingEntryComponent.Factory, + ): OnboardingEntryComponent.Factory +} + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + @Binds + @IntoMap + @ClassKey(OnboardingEntryModel::class) + fun provideModel(model: OnboardingEntryModel): Model +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt new file mode 100644 index 0000000000..9a040ff17c --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -0,0 +1,58 @@ +package com.tangem.features.onboarding.v2.entry.impl.model + +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.scan.ProductType +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.features.onboarding.v2.TitleProvider +import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent +import com.tangem.features.onboarding.v2.entry.impl.model.state.OnboardingState +import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + +@ComponentScoped +internal class OnboardingEntryModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, +) : Model() { + + private val params = paramsContainer.require() + + val titleProvider = object : TitleProvider { + override val currentTitle = MutableStateFlow(stringReference("")) + override fun changeTitle(text: TextReference) { + currentTitle.value = text + } + } + + val state = MutableStateFlow( + OnboardingState( + currentRoute = routeByProductType(params.scanResponse), + ), + ) + + private fun routeByProductType(scanResponse: ScanResponse): OnboardingRoute { + return when (scanResponse.productType) { + ProductType.Note -> TODO() + ProductType.Twins -> TODO() + ProductType.Wallet -> OnboardingRoute.Wallet12( + scanResponse = scanResponse, + withSeedPhraseFlow = false, + titleProvider = titleProvider, + ) + ProductType.Wallet2 -> OnboardingRoute.Wallet12( + scanResponse = scanResponse, + withSeedPhraseFlow = true, + titleProvider = titleProvider, + ) + ProductType.Start2Coin -> TODO() + ProductType.Ring -> TODO() + ProductType.Visa -> TODO() + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/state/OnboardingState.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/state/OnboardingState.kt new file mode 100644 index 0000000000..66e5d7c4c4 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/state/OnboardingState.kt @@ -0,0 +1,7 @@ +package com.tangem.features.onboarding.v2.entry.impl.model.state + +import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute + +internal data class OnboardingState( + val currentRoute: OnboardingRoute, +) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt new file mode 100644 index 0000000000..3cd1b811a8 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingChildFactory.kt @@ -0,0 +1,24 @@ +package com.tangem.features.onboarding.v2.entry.impl.routing + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent +import javax.inject.Inject + +internal class OnboardingChildFactory @Inject constructor( + private val wallet12ComponentFactory: OnboardingMultiWalletComponent.Factory, +) { + + fun createChild(route: OnboardingRoute, childContext: AppComponentContext): Any { + return when (route) { + is OnboardingRoute.Wallet12 -> wallet12ComponentFactory.create( + context = childContext, + params = OnboardingMultiWalletComponent.Params( + scanResponse = route.scanResponse, + withSeedPhraseFlow = route.withSeedPhraseFlow, + titleProvider = route.titleProvider, + ), + ) + else -> Unit + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt new file mode 100644 index 0000000000..4dbd6593f0 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/routing/OnboardingRoute.kt @@ -0,0 +1,16 @@ +package com.tangem.features.onboarding.v2.entry.impl.routing + +import com.tangem.core.decompose.navigation.Route +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.features.onboarding.v2.TitleProvider + +sealed class OnboardingRoute : Route { + + data object None : OnboardingRoute() + + data class Wallet12( + val titleProvider: TitleProvider, + val scanResponse: ScanResponse, + val withSeedPhraseFlow: Boolean, + ) : OnboardingRoute() +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt new file mode 100644 index 0000000000..16400dd642 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/ui/OnboardingEntry.kt @@ -0,0 +1,43 @@ +package com.tangem.features.onboarding.v2.entry.impl.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children +import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation +import com.arkivanov.decompose.router.stack.ChildStack +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute +import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent + +@Composable +internal inline fun OnboardingEntry( + modifier: Modifier = Modifier, + childStack: ChildStack, + stepperContent: @Composable (Modifier) -> Unit, +) { + Column( + modifier = modifier + .fillMaxSize() + .statusBarsPadding() + .background(TangemTheme.colors.background.primary), + ) { + stepperContent(Modifier.fillMaxWidth()) + + Children( + stack = childStack, + animation = stackAnimation(slide()), + ) { + when (it.configuration) { + is OnboardingRoute.Wallet12 -> { + (it.instance as OnboardingMultiWalletComponent).Content( + modifier = modifier, + ) + } + OnboardingRoute.None -> {} + } + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/api/OnboardingMultiWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/api/OnboardingMultiWalletComponent.kt new file mode 100644 index 0000000000..1dedfd3d41 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/api/OnboardingMultiWalletComponent.kt @@ -0,0 +1,18 @@ +package com.tangem.features.onboarding.v2.multiwallet.api + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.decompose.navigation.inner.InnerNavigationHolder +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.features.onboarding.v2.TitleProvider + +interface OnboardingMultiWalletComponent : ComposableContentComponent, InnerNavigationHolder { + + data class Params( + val titleProvider: TitleProvider, + val scanResponse: ScanResponse, + val withSeedPhraseFlow: Boolean, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt new file mode 100644 index 0000000000..8cc7a9e553 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/DefaultOnboardingMultiWalletComponent.kt @@ -0,0 +1,58 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.decompose.navigation.inner.InnerNavigation +import com.tangem.core.decompose.navigation.inner.InnerNavigationState +import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent +import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel +import com.tangem.features.onboarding.v2.multiwallet.impl.ui.OnboardingMultiWallet +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow + +internal class DefaultOnboardingMultiWalletComponent @AssistedInject constructor( + @Assisted private val context: AppComponentContext, + @Assisted private val params: OnboardingMultiWalletComponent.Params, +) : OnboardingMultiWalletComponent, AppComponentContext by context { + + private val model: OnboardingMultiWalletModel = getOrCreateModel(params) + + override val innerNavigation: InnerNavigation = object : InnerNavigation { + override val state = MutableStateFlow( + Wallet12InnerNavigationState(1, 5), // TODO + ) + + override fun pop(onComplete: (Boolean) -> Unit) { + // TODO + } + } + + @Composable + override fun Content(modifier: Modifier) { + val uiState by model.uiState.collectAsStateWithLifecycle() + + OnboardingMultiWallet( + modifier = modifier, + state = uiState, + ) + } + + @AssistedFactory + interface Factory : OnboardingMultiWalletComponent.Factory { + override fun create( + context: AppComponentContext, + params: OnboardingMultiWalletComponent.Params, + ): DefaultOnboardingMultiWalletComponent + } +} + +data class Wallet12InnerNavigationState( + override val stackSize: Int, + override val stackMaxSize: Int?, +) : InnerNavigationState \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..1c7fd39182 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/di/ComponentModule.kt @@ -0,0 +1,33 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent +import com.tangem.features.onboarding.v2.multiwallet.impl.DefaultOnboardingMultiWalletComponent +import com.tangem.features.onboarding.v2.multiwallet.impl.model.OnboardingMultiWalletModel +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 +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindComponent(factory: DefaultOnboardingMultiWalletComponent.Factory): OnboardingMultiWalletComponent.Factory +} + +@Module +@InstallIn(DecomposeComponent::class) +internal interface ModelModule { + + @Binds + @IntoMap + @ClassKey(OnboardingMultiWalletModel::class) + fun provideModel(model: OnboardingMultiWalletModel): Model +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Dialogs.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Dialogs.kt new file mode 100644 index 0000000000..f6905984f6 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Dialogs.kt @@ -0,0 +1,16 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.model + +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM + +internal fun resetCardDialog(onConfirm: () -> Unit, onDismiss: () -> Unit, onDismissButtonClick: () -> Unit) = + OnboardingMultiWalletUM.Dialog( + title = resourceReference(R.string.onboarding_activation_error_title), + description = resourceReference(R.string.onboarding_activation_error_message), + confirmButtonText = resourceReference(R.string.common_ok), + dismissButtonText = resourceReference(R.string.common_support), + onDismiss = onDismiss, + onConfirm = onConfirm, + onDismissButtonClick = onDismissButtonClick, + ) \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt new file mode 100644 index 0000000000..f031b2a3f6 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletModel.kt @@ -0,0 +1,140 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.model + +import com.tangem.common.CompletionResult +import com.tangem.common.core.TangemSdkError +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.scan.CardDTO +import com.tangem.features.onboarding.v2.multiwallet.api.OnboardingMultiWalletComponent +import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM +import com.tangem.sdk.api.TangemSdkManager +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ComponentScoped +internal class OnboardingMultiWalletModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val tangemSdkManager: TangemSdkManager, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val getCardInfoUseCase: GetCardInfoUseCase, +) : Model() { + + private val params = paramsContainer.require() + private val currentScanResponse = MutableStateFlow(params.scanResponse) + + val state = MutableStateFlow( + OnboardingMultiWalletState( + currentStep = getInitialStep(), + ), + ) + + val uiState = MutableStateFlow( + OnboardingMultiWalletUM( + onCreateWalletClick = { createWallet(false) }, + showSeedPhraseOption = params.withSeedPhraseFlow, + onOtherOptionsClick = { /* navigate */ }, + onBack = { }, + dialog = null, + ), + ) + + init { + initScreenTitleSub() + } + + private fun getInitialStep(): OnboardingMultiWalletState.Step { + val card = currentScanResponse.value.card + + return when { + card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup -> + OnboardingMultiWalletState.Step.AddBackupDevice + card.wallets.isNotEmpty() && card.backupStatus?.isActive == true -> + OnboardingMultiWalletState.Step.FinishBackup + else -> + OnboardingMultiWalletState.Step.CreateWallet + } + } + + private fun initScreenTitleSub() { + val title = screenTitleByStep(getInitialStep()) + params.titleProvider.changeTitle(title) + + modelScope.launch { + state + .map { it.currentStep } + .collectLatest { step -> + val title = screenTitleByStep(step) + params.titleProvider.changeTitle(title) + } + } + } + + private fun createWallet(shouldReset: Boolean) { + modelScope.launch { + val result = tangemSdkManager.createProductWallet( + scanResponse = currentScanResponse.value, + shouldReset = shouldReset, + ) + + when (result) { + is CompletionResult.Success -> { + currentScanResponse.update { + it.copy( + card = result.data.card, + derivedKeys = result.data.derivedKeys, + primaryCard = result.data.primaryCard, + ) + } + + // TODO + // Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully()) + } + + is CompletionResult.Failure -> { + if (result.error is TangemSdkError.WalletAlreadyCreated) { + // show should reset dialog + handleActivationError() + } + } + } + } + } + + private fun handleActivationError() { + uiState.update { + it.copy( + dialog = resetCardDialog( + onConfirm = { + uiState.update { it.copy(dialog = null) } + resetCard() + }, + onDismiss = { + uiState.update { it.copy(dialog = null) } + }, + onDismissButtonClick = ::navigateToSupportScreen, + ), + ) + } + } + + private fun resetCard() { + createWallet(true) + } + + fun navigateToSupportScreen() { + modelScope.launch { + val cardInfo = getCardInfoUseCase(currentScanResponse.value).getOrNull() ?: return@launch + sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo)) + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletState.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletState.kt new file mode 100644 index 0000000000..d1b11653dd --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/OnboardingMultiWalletState.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.model + +data class OnboardingMultiWalletState( + val currentStep: Step, +) { + + enum class Step { + GeneratePrivateKeys, CreateWallet, AddBackupDevice, FinishBackup, Done + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Utils.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Utils.kt new file mode 100644 index 0000000000..fb735811de --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/model/Utils.kt @@ -0,0 +1,13 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.model + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onboarding.v2.impl.R + +fun screenTitleByStep(step: OnboardingMultiWalletState.Step): TextReference = when (step) { + OnboardingMultiWalletState.Step.GeneratePrivateKeys -> TODO() + OnboardingMultiWalletState.Step.CreateWallet -> resourceReference(R.string.onboarding_create_wallet_header) + OnboardingMultiWalletState.Step.AddBackupDevice -> TODO() + OnboardingMultiWalletState.Step.FinishBackup -> TODO() + OnboardingMultiWalletState.Step.Done -> TODO() +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt new file mode 100644 index 0000000000..d2de011033 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/OnboardingMultiWallet.kt @@ -0,0 +1,79 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.ui + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.BasicDialog +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.multiwallet.impl.ui.state.OnboardingMultiWalletUM + +@Composable +internal fun OnboardingMultiWallet(state: OnboardingMultiWalletUM, modifier: Modifier = Modifier) { + if (state.dialog != null) { + BasicDialog( + title = state.dialog.title.resolveReference(), + message = state.dialog.description.resolveReference(), + confirmButton = DialogButtonUM( + title = state.dialog.confirmButtonText.resolveReference(), + onClick = state.dialog.onConfirm, + ), + dismissButton = DialogButtonUM( + title = state.dialog.dismissButtonText.resolveReference(), + onClick = state.dialog.onDismissButtonClick, + ), + onDismissDialog = state.dialog.onDismiss, + ) + } + + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding() + .verticalScroll(rememberScrollState()), + ) { + Box(Modifier.weight(1f)) + + PrimaryButtonIconEnd( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 12.dp) + .fillMaxWidth(), + iconResId = R.drawable.ic_tangem_24, + text = stringResource(R.string.onboarding_create_wallet_button_create_wallet), + onClick = state.onCreateWalletClick, + ) + + SecondaryButton( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + text = stringResource(R.string.onboarding_create_wallet_options_button_options), + onClick = state.onOtherOptionsClick, + ) + } +} + +@Preview +@Composable +private fun Preview() { + TangemThemePreview { + OnboardingMultiWallet( + state = OnboardingMultiWalletUM( + onCreateWalletClick = {}, + showSeedPhraseOption = true, + onBack = {}, + onOtherOptionsClick = {}, + dialog = null, + ), + ) + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt new file mode 100644 index 0000000000..8ae99c8da0 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/ui/state/OnboardingMultiWalletUM.kt @@ -0,0 +1,21 @@ +package com.tangem.features.onboarding.v2.multiwallet.impl.ui.state + +import com.tangem.core.ui.extensions.TextReference + +internal data class OnboardingMultiWalletUM( + val onCreateWalletClick: () -> Unit, + val showSeedPhraseOption: Boolean, + val onOtherOptionsClick: () -> Unit, + val onBack: () -> Unit, + val dialog: Dialog?, +) { + data class Dialog( + val title: TextReference, + val description: TextReference, + val dismissButtonText: TextReference, + val confirmButtonText: TextReference, + val onConfirm: () -> Unit, + val onDismissButtonClick: () -> Unit, + val onDismiss: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/api/OnboardingStepperComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/api/OnboardingStepperComponent.kt new file mode 100644 index 0000000000..8943397d3d --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/api/OnboardingStepperComponent.kt @@ -0,0 +1,28 @@ +package com.tangem.features.onboarding.v2.stepper.api + +import androidx.annotation.IntRange +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.scan.ScanResponse +import kotlinx.coroutines.flow.MutableStateFlow + +internal interface OnboardingStepperComponent : ComposableContentComponent { + + data class StepperState( + @IntRange(from = 0) val currentStep: Int, + @IntRange(from = 0) val steps: Int, + val title: TextReference, + val showProgress: Boolean, + ) + + class Params( + val initState: StepperState, + val popBack: () -> Unit, + val scanResponse: ScanResponse, + ) + + val state: MutableStateFlow + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt new file mode 100644 index 0000000000..2f4253b9f6 --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt @@ -0,0 +1,56 @@ +package com.tangem.features.onboarding.v2.stepper.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.essenty.instancekeeper.getOrCreateSimple +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent +import com.tangem.features.onboarding.v2.stepper.impl.ui.OnboardingStepper +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch + +internal class DefaultOnboardingStepperComponent @AssistedInject constructor( + @Assisted val context: AppComponentContext, + @Assisted val params: OnboardingStepperComponent.Params, + private val getCardInfoUseCase: GetCardInfoUseCase, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, +) : OnboardingStepperComponent, AppComponentContext by context { + + override val state = instanceKeeper.getOrCreateSimple { MutableStateFlow(params.initState) } + + private fun openSupport() { + componentScope.launch { + val cardInfo = getCardInfoUseCase(params.scanResponse).getOrNull() ?: return@launch + sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo)) + } + } + + @Composable + override fun Content(modifier: Modifier) { + val state by state.collectAsStateWithLifecycle() + + OnboardingStepper( + state = state, + onBackClick = remember(this) { params.popBack }, + onSupportButtonClick = remember(this) { ::openSupport }, + modifier = modifier, + ) + } + + @AssistedFactory + interface Factory : OnboardingStepperComponent.Factory { + override fun create( + context: AppComponentContext, + params: OnboardingStepperComponent.Params, + ): DefaultOnboardingStepperComponent + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/di/ComponentModule.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/di/ComponentModule.kt new file mode 100644 index 0000000000..e0401a486d --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/di/ComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onboarding.v2.stepper.impl.di + +import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent +import com.tangem.features.onboarding.v2.stepper.impl.DefaultOnboardingStepperComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface ComponentModule { + + @Binds + @Singleton + fun bindOnboardingStepperComponent( + factory: DefaultOnboardingStepperComponent.Factory, + ): OnboardingStepperComponent.Factory +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt new file mode 100644 index 0000000000..a22c50812e --- /dev/null +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/ui/OnboardingStepper.kt @@ -0,0 +1,98 @@ +package com.tangem.features.onboarding.v2.stepper.impl.ui + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.progressbar.LinearProgressIndicator +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemAnimations +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onboarding.v2.impl.R +import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent + +@Composable +internal fun OnboardingStepper( + state: OnboardingStepperComponent.StepperState, + onBackClick: () -> Unit, + onSupportButtonClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val fraction = state.currentStep.toFloat() / state.steps.coerceAtLeast(1) + val animatedIndicatorFraction by TangemAnimations.horizontalIndicatorAsState(targetFraction = fraction) + + val progressAlpha by animateFloatAsState( + targetValue = if (state.showProgress) 1f else 0f, + animationSpec = tween( + durationMillis = 300, + easing = LinearEasing, + ), + label = "progressAlpha", + ) + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + TangemTopAppBar( + startButton = TopAppBarButtonUM.Back(onBackClick), + endButton = TopAppBarButtonUM(iconRes = R.drawable.ic_chat_24, onIconClicked = onSupportButtonClick) + .takeIf { state.steps != state.currentStep }, + title = if (state.steps == state.currentStep) { + resourceReference(R.string.common_done) + } else { + state.title + }, + containerColor = TangemTheme.colors.background.primary, + modifier = modifier, + ) + + LinearProgressIndicator( + modifier = Modifier + .alpha(progressAlpha) + .padding(horizontal = 16.dp) + .height(4.dp) + .fillMaxWidth(), + progress = { animatedIndicatorFraction }, + color = TangemTheme.colors.icon.primary1, + backgroundColor = TangemTheme.colors.background.tertiary, + strokeCap = StrokeCap.Round, + ) + } +} + +@Preview +@Composable +private fun Preview() { + TangemThemePreview { + Box( + Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary), + ) { + OnboardingStepper( + modifier = Modifier.align(Alignment.TopCenter), + state = OnboardingStepperComponent.StepperState( + currentStep = 2, + steps = 3, + title = resourceReference(R.string.common_done), + showProgress = true, + ), + onBackClick = {}, + onSupportButtonClick = {}, + ) + } + } +} \ No newline at end of file diff --git a/features/onramp/api/.gitignore b/features/onramp/api/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/onramp/api/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/onramp/api/build.gradle.kts b/features/onramp/api/build.gradle.kts new file mode 100644 index 0000000000..cde16c9b92 --- /dev/null +++ b/features/onramp/api/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("kotlin-parcelize") + id("configuration") +} + +android { + namespace = "com.tangem.features.onramp.api" +} + +dependencies { + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /* Project - Domain */ + implementation(projects.domain.wallets.models) + + /* Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt new file mode 100644 index 0000000000..7d46cb6ad1 --- /dev/null +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/OnrampFeatureToggles.kt @@ -0,0 +1,6 @@ +package com.tangem.features.onramp + +interface OnrampFeatureToggles { + + val isFeatureEnabled: Boolean +} \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/BuyCryptoComponent.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/BuyCryptoComponent.kt new file mode 100644 index 0000000000..c627160e26 --- /dev/null +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/BuyCryptoComponent.kt @@ -0,0 +1,22 @@ +package com.tangem.features.onramp.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Buy crypto component + * +[REDACTED_AUTHOR] + */ +interface BuyCryptoComponent : ComposableContentComponent { + + interface Factory : ComponentFactory + + /** + * Params + * + * @property userWalletId user wallet id + */ + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt new file mode 100644 index 0000000000..16ac6b24f7 --- /dev/null +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampComponent.kt @@ -0,0 +1,11 @@ +package com.tangem.features.onramp.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent + +interface OnrampComponent : ComposableContentComponent { + + class Params + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/SellCryptoComponent.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/SellCryptoComponent.kt new file mode 100644 index 0000000000..ce340b8c07 --- /dev/null +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/SellCryptoComponent.kt @@ -0,0 +1,22 @@ +package com.tangem.features.onramp.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Sell crypto component + * +[REDACTED_AUTHOR] + */ +interface SellCryptoComponent : ComposableContentComponent { + + interface Factory : ComponentFactory + + /** + * Params + * + * @property userWalletId user wallet id + */ + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file diff --git a/features/onramp/impl/.gitignore b/features/onramp/impl/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/features/onramp/impl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts new file mode 100644 index 0000000000..1ff3b4c39c --- /dev/null +++ b/features/onramp/impl/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.onramp.impl" +} + +dependencies { + /** Project - API */ + implementation(projects.features.onramp.api) + + /** Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + implementation(projects.core.featuretoggles) + + /** Project - Common */ + implementation(projects.common.ui) + + /** Project - Domain */ + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.balanceHiding) + implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.legacy) + implementation(projects.domain.models) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + + /** DI */ + implementation(deps.hilt.android) + implementation(project(":common:ui")) + kapt(deps.hilt.kapt) + + /** AndroidX */ + implementation(deps.androidx.activity.compose) + implementation(deps.lifecycle.compose) + + /** Compose */ + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.accompanist.systemUiController) + implementation(deps.compose.foundation) + implementation(deps.compose.material3) + implementation(deps.compose.shimmer) + implementation(deps.compose.coil) + implementation(deps.compose.material) + + /** Other */ + implementation(deps.decompose.ext.compose) + implementation(deps.kotlin.immutable.collections) + implementation(deps.reKotlin) + implementation(deps.timber) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt new file mode 100644 index 0000000000..15d7dd6713 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/DefaultOnrampFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.features.onramp + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager + +internal class DefaultOnrampFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : OnrampFeatureToggles { + + override val isFeatureEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("ONRAMP_ENABLED") +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/DefaultBuyCryptoComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/DefaultBuyCryptoComponent.kt new file mode 100644 index 0000000000..8e36bf09b0 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/DefaultBuyCryptoComponent.kt @@ -0,0 +1,36 @@ +package com.tangem.features.onramp.buy + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.features.onramp.component.BuyCryptoComponent +import com.tangem.features.onramp.entity.OnrampOperation +import com.tangem.features.onramp.selecttoken.OnrampOperationComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Stable +internal class DefaultBuyCryptoComponent @AssistedInject constructor( + onrampOperationComponentFactory: OnrampOperationComponent.Factory, + @Assisted private val appComponentContext: AppComponentContext, + @Assisted private val params: BuyCryptoComponent.Params, +) : BuyCryptoComponent { + + private val selectTokenComponent: OnrampOperationComponent = onrampOperationComponentFactory.create( + context = appComponentContext, + params = OnrampOperationComponent.Params(operation = OnrampOperation.BUY, userWalletId = params.userWalletId), + ) + + @Composable + override fun Content(modifier: Modifier) { + selectTokenComponent.Content(modifier = modifier) + } + + @AssistedFactory + interface Factory : BuyCryptoComponent.Factory { + + override fun create(context: AppComponentContext, params: BuyCryptoComponent.Params): DefaultBuyCryptoComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/di/BuyCryptoComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/di/BuyCryptoComponentModule.kt new file mode 100644 index 0000000000..d45a326a89 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/buy/di/BuyCryptoComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.onramp.buy.di + +import com.tangem.features.onramp.buy.DefaultBuyCryptoComponent +import com.tangem.features.onramp.component.BuyCryptoComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface BuyCryptoComponentModule { + + @Binds + @Singleton + fun bindBuyCryptoComponentFactory(factory: DefaultBuyCryptoComponent.Factory): BuyCryptoComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/ConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/ConfirmResidencyComponent.kt new file mode 100644 index 0000000000..74742aa575 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/ConfirmResidencyComponent.kt @@ -0,0 +1,16 @@ +package com.tangem.features.onramp.component + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent + +internal interface ConfirmResidencyComponent : ComposableBottomSheetComponent { + + data class Params( + val countryName: String, + val isOnrampSupported: Boolean, + val countryFlagUrl: String, + val onDismiss: () -> Unit, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultConfirmResidencyComponent.kt new file mode 100644 index 0000000000..177e697eb8 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultConfirmResidencyComponent.kt @@ -0,0 +1,55 @@ +package com.tangem.features.onramp.component.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.features.onramp.component.ConfirmResidencyComponent +import com.tangem.features.onramp.model.ConfirmResidencyModel +import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheet +import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheetContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultConfirmResidencyComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: ConfirmResidencyComponent.Params, +) : ConfirmResidencyComponent, AppComponentContext by context { + + private val model: ConfirmResidencyModel = getOrCreateModel(params) + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val state by model.state.collectAsStateWithLifecycle() + val bottomSheetConfig = remember { + TangemBottomSheetConfig( + isShow = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ) + } + ConfirmResidencyBottomSheet( + config = bottomSheetConfig, + content = { modifier -> + ConfirmResidencyBottomSheetContent(model = state, modifier = modifier) + }, + ) + } + + @AssistedFactory + interface Factory : ConfirmResidencyComponent.Factory { + override fun create( + context: AppComponentContext, + params: ConfirmResidencyComponent.Params, + ): DefaultConfirmResidencyComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultOnrampComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultOnrampComponent.kt new file mode 100644 index 0000000000..7ec0fb692a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/impl/DefaultOnrampComponent.kt @@ -0,0 +1,64 @@ +package com.tangem.features.onramp.component.impl + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.onramp.component.OnrampComponent +import com.tangem.features.onramp.component.ConfirmResidencyComponent +import com.tangem.features.onramp.entity.OnrampBottomSheetConfig +import com.tangem.features.onramp.model.OnrampModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultOnrampComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: OnrampComponent.Params, + private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory, +) : OnrampComponent, AppComponentContext by context { + + private val model: OnrampModel = getOrCreateModel(params) + private val confirmResidencyBottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = null, + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + + @Composable + override fun Content(modifier: Modifier) { + val bottomSheet by confirmResidencyBottomSheetSlot.subscribeAsState() + BackHandler(onBack = router::pop) + + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + config: OnrampBottomSheetConfig, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = when (config) { + OnrampBottomSheetConfig.ConfirmResidency -> confirmResidencyComponentFactory.create( + context = childByContext(componentContext), + params = ConfirmResidencyComponent.Params( + countryName = "United States", + isOnrampSupported = true, + countryFlagUrl = "https://hatscripts.github.io/circle-flags/flags/us.svg", + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + } + + @AssistedFactory + interface Factory : OnrampComponent.Factory { + override fun create(context: AppComponentContext, params: OnrampComponent.Params): DefaultOnrampComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/preview/PreviewConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/preview/PreviewConfirmResidencyComponent.kt new file mode 100644 index 0000000000..b5e167f157 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/component/preview/PreviewConfirmResidencyComponent.kt @@ -0,0 +1,46 @@ +package com.tangem.features.onramp.component.preview + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.stringReference +import com.tangem.features.onramp.component.ConfirmResidencyComponent +import com.tangem.features.onramp.entity.ConfirmResidencyUM +import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheet +import com.tangem.features.onramp.ui.ConfirmResidencyBottomSheetContent +import kotlinx.coroutines.flow.MutableStateFlow + +internal class PreviewConfirmResidencyComponent( + initialState: ConfirmResidencyUM = ConfirmResidencyUM( + country = "United States", + countryFlagUrl = "https://hatscripts.github.io/circle-flags/flags/us.svg", + isCountrySupported = true, + primaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig(onClick = {}, text = stringReference("Confirm")), + secondaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig(onClick = {}, text = stringReference("Change")), + ), +) : ConfirmResidencyComponent { + + private val previewState: MutableStateFlow = MutableStateFlow(initialState) + + override fun dismiss() { + /* no-op */ + } + + @Composable + override fun BottomSheet() { + val state by previewState.collectAsStateWithLifecycle() + val bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ) + ConfirmResidencyBottomSheet( + config = bottomSheetConfig, + content = { modifier -> + ConfirmResidencyBottomSheetContent(model = state, modifier = modifier) + }, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampComponentModule.kt new file mode 100644 index 0000000000..6f27d391db --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampComponentModule.kt @@ -0,0 +1,26 @@ +package com.tangem.features.onramp.di + +import com.tangem.features.onramp.component.ConfirmResidencyComponent +import com.tangem.features.onramp.component.OnrampComponent +import com.tangem.features.onramp.component.impl.DefaultConfirmResidencyComponent +import com.tangem.features.onramp.component.impl.DefaultOnrampComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface OnrampComponentModule { + + @Binds + @Singleton + fun bindOnrampComponentFactory(factory: DefaultOnrampComponent.Factory): OnrampComponent.Factory + + @Binds + @Singleton + fun bindConfirmResidencyComponentFactory( + factory: DefaultConfirmResidencyComponent.Factory, + ): ConfirmResidencyComponent.Factory +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/FeatureModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampFeatureModule.kt similarity index 53% rename from features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/FeatureModule.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampFeatureModule.kt index a2085dfe77..cac4df29b9 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/di/FeatureModule.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampFeatureModule.kt @@ -1,8 +1,8 @@ -package com.tangem.features.markets.di +package com.tangem.features.onramp.di import com.tangem.core.featuretoggle.manager.FeatureTogglesManager -import com.tangem.features.markets.DefaultMarketsFeatureToggles -import com.tangem.features.markets.MarketsFeatureToggles +import com.tangem.features.onramp.DefaultOnrampFeatureToggles +import com.tangem.features.onramp.OnrampFeatureToggles import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -11,10 +11,11 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object FeatureModule { +internal object OnrampFeatureModule { @Provides @Singleton - fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): MarketsFeatureToggles = - DefaultMarketsFeatureToggles(featureTogglesManager = featureTogglesManager) + fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): OnrampFeatureToggles { + return DefaultOnrampFeatureToggles(featureTogglesManager) + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampModelModule.kt new file mode 100644 index 0000000000..41f90f6d5a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/di/OnrampModelModule.kt @@ -0,0 +1,26 @@ +package com.tangem.features.onramp.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.model.OnrampModel +import com.tangem.features.onramp.model.ConfirmResidencyModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface OnrampModelModule { + + @Binds + @IntoMap + @ClassKey(OnrampModel::class) + fun provideOnrampModel(model: OnrampModel): Model + + @Binds + @IntoMap + @ClassKey(ConfirmResidencyModel::class) + fun provideConfirmResidencyModel(model: ConfirmResidencyModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyUM.kt new file mode 100644 index 0000000000..106df7e1bc --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/ConfirmResidencyUM.kt @@ -0,0 +1,16 @@ +package com.tangem.features.onramp.entity + +import com.tangem.core.ui.extensions.TextReference + +data class ConfirmResidencyUM( + val country: String, + val countryFlagUrl: String, + val isCountrySupported: Boolean, + val primaryButtonConfig: ActionButtonConfig, + val secondaryButtonConfig: ActionButtonConfig, +) { + data class ActionButtonConfig( + val onClick: () -> Unit, + val text: TextReference, + ) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/OnrampBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/OnrampBottomSheetConfig.kt new file mode 100644 index 0000000000..81289b4cdf --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/OnrampBottomSheetConfig.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onramp.entity + +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class OnrampBottomSheetConfig { + + @Serializable + data object ConfirmResidency : OnrampBottomSheetConfig() +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/OnrampOperation.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/OnrampOperation.kt new file mode 100644 index 0000000000..1a94304325 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/entity/OnrampOperation.kt @@ -0,0 +1,12 @@ +package com.tangem.features.onramp.entity + +/** + * Onramp operation + * +[REDACTED_AUTHOR] + */ +internal enum class OnrampOperation { + BUY, + SELL, + ; +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/ConfirmResidencyModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/ConfirmResidencyModel.kt new file mode 100644 index 0000000000..9457285bd3 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/ConfirmResidencyModel.kt @@ -0,0 +1,53 @@ +package com.tangem.features.onramp.model + +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onramp.component.ConfirmResidencyComponent +import com.tangem.features.onramp.entity.ConfirmResidencyUM +import com.tangem.features.onramp.impl.R +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + +@ComponentScoped +internal class ConfirmResidencyModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: ConfirmResidencyComponent.Params = paramsContainer.require() + val state: MutableStateFlow = MutableStateFlow( + ConfirmResidencyUM( + country = params.countryName, + countryFlagUrl = params.countryFlagUrl, + isCountrySupported = params.isOnrampSupported, + primaryButtonConfig = getPrimaryButtonConfig(), + secondaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig( + onClick = ::onChangeClick, + text = resourceReference(R.string.common_change), + ), + ), + ) + + private fun getPrimaryButtonConfig() = if (params.isOnrampSupported) { + ConfirmResidencyUM.ActionButtonConfig( + onClick = params.onDismiss, + text = resourceReference(R.string.common_confirm), + ) + } else { + ConfirmResidencyUM.ActionButtonConfig( + onClick = ::onCloseClick, + text = resourceReference(R.string.common_close), + ) + } + + @Suppress("EmptyFunctionBlock") + private fun onCloseClick() { + } + + private fun onChangeClick() { + // TODO: [REDACTED_JIRA] + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/OnrampModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/OnrampModel.kt new file mode 100644 index 0000000000..d1f35e1fca --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/model/OnrampModel.kt @@ -0,0 +1,38 @@ +package com.tangem.features.onramp.model + +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.core.decompose.di.ComponentScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.features.onramp.component.OnrampComponent +import com.tangem.features.onramp.entity.OnrampBottomSheetConfig +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("MagicNumber") +@ComponentScoped +internal class OnrampModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + paramsContainer: ParamsContainer, +) : Model() { + + @Suppress("UnusedPrivateMember") + private val params: OnrampComponent.Params = paramsContainer.require() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + + init { + modelScope.launch { + delay(1500) + bottomSheetNavigation.activate(OnrampBottomSheetConfig.ConfirmResidency) + } + } + + fun pop() { + router.pop() + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt new file mode 100644 index 0000000000..454ba6bea6 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt @@ -0,0 +1,84 @@ +package com.tangem.features.onramp.selecttoken + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import arrow.core.getOrElse +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.tokens.legacy.TradeCryptoAction +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.onramp.entity.OnrampOperation +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.selecttoken.ui.OnrampSelectToken +import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.launch + +internal class DefaultOnrampOperationComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + onrampTokenListComponentFactory: OnrampTokenListComponent.Factory, + @Assisted private val params: OnrampOperationComponent.Params, + private val reduxStateHolder: ReduxStateHolder, + private val getWalletsUseCase: GetWalletsUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, +) : AppComponentContext by appComponentContext, OnrampOperationComponent { + + private val onrampTokenListComponent: OnrampTokenListComponent = onrampTokenListComponentFactory.create( + context = child(key = "token_list"), + params = OnrampTokenListComponent.Params( + filterOperation = params.operation, + hasSearchBar = true, + userWalletId = params.userWalletId, + onTokenClick = ::onTokenClick, + ), + ) + + @Composable + override fun Content(modifier: Modifier) { + OnrampSelectToken( + titleResId = when (params.operation) { + OnrampOperation.BUY -> R.string.common_buy + OnrampOperation.SELL -> R.string.common_sell + }, + onBackClick = router::pop, + onrampTokenListComponent = onrampTokenListComponent, + modifier = modifier, + ) + } + + private fun onTokenClick(status: CryptoCurrencyStatus) { + componentScope.launch { + val appCurrencyCode = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }.code + + reduxStateHolder.dispatch( + when (params.operation) { + OnrampOperation.BUY -> getBuyAction(status, appCurrencyCode) + OnrampOperation.SELL -> TradeCryptoAction.Sell(status, appCurrencyCode) + }, + ) + } + } + + private fun getBuyAction(status: CryptoCurrencyStatus, appCurrencyCode: String): TradeCryptoAction { + return TradeCryptoAction.Buy( + userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }, + cryptoCurrencyStatus = status, + appCurrencyCode = appCurrencyCode, + ) + } + + @AssistedFactory + interface Factory : OnrampOperationComponent.Factory { + + override fun create( + context: AppComponentContext, + params: OnrampOperationComponent.Params, + ): DefaultOnrampOperationComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/OnrampOperationComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/OnrampOperationComponent.kt new file mode 100644 index 0000000000..c35a452d58 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/OnrampOperationComponent.kt @@ -0,0 +1,24 @@ +package com.tangem.features.onramp.selecttoken + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.onramp.entity.OnrampOperation + +/** + * Base operation component + * +[REDACTED_AUTHOR] + */ +internal interface OnrampOperationComponent : ComposableContentComponent { + + interface Factory : ComponentFactory + + /** + * Params + * + * @property operation operation + * @property userWalletId id of multi-currency wallet + */ + data class Params(val operation: OnrampOperation, val userWalletId: UserWalletId) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/di/OnrampOperationComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/di/OnrampOperationComponentModule.kt new file mode 100644 index 0000000000..b15e75f360 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/di/OnrampOperationComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.selecttoken.di + +import com.tangem.features.onramp.selecttoken.DefaultOnrampOperationComponent +import com.tangem.features.onramp.selecttoken.OnrampOperationComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface OnrampOperationComponentModule { + + @Binds + @Singleton + fun bindOnrampOperationComponentFactory( + factory: DefaultOnrampOperationComponent.Factory, + ): OnrampOperationComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt new file mode 100644 index 0000000000..653883fb90 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt @@ -0,0 +1,45 @@ +package com.tangem.features.onramp.selecttoken.ui + +import androidx.activity.compose.BackHandler +import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent + +@Composable +internal fun OnrampSelectToken( + @StringRes titleResId: Int, + onBackClick: () -> Unit, + onrampTokenListComponent: OnrampTokenListComponent, + modifier: Modifier = Modifier, +) { + BackHandler(onBack = onBackClick) + + Column( + modifier = modifier + .background(TangemTheme.colors.background.secondary) + .imePadding() + .systemBarsPadding(), + ) { + AppBarWithBackButton( + onBackClick = onBackClick, + text = stringResource(id = titleResId), + iconRes = R.drawable.ic_close_24, + ) + + onrampTokenListComponent.Content( + contentPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp), + modifier = Modifier, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/sell/DefaultSellCryptoComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/sell/DefaultSellCryptoComponent.kt new file mode 100644 index 0000000000..f7d63936d1 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/sell/DefaultSellCryptoComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.onramp.sell + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.features.onramp.component.SellCryptoComponent +import com.tangem.features.onramp.entity.OnrampOperation +import com.tangem.features.onramp.selecttoken.OnrampOperationComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Stable +internal class DefaultSellCryptoComponent @AssistedInject constructor( + onrampOperationComponentFactory: OnrampOperationComponent.Factory, + @Assisted private val appComponentContext: AppComponentContext, + @Assisted private val params: SellCryptoComponent.Params, +) : SellCryptoComponent { + + private val selectTokenComponent: OnrampOperationComponent = onrampOperationComponentFactory.create( + context = appComponentContext, + params = OnrampOperationComponent.Params(operation = OnrampOperation.SELL, userWalletId = params.userWalletId), + ) + + @Composable + override fun Content(modifier: Modifier) { + selectTokenComponent.Content(modifier = modifier) + } + + @AssistedFactory + interface Factory : SellCryptoComponent.Factory { + + override fun create( + context: AppComponentContext, + params: SellCryptoComponent.Params, + ): DefaultSellCryptoComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/sell/di/SellCryptoComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/sell/di/SellCryptoComponentModule.kt new file mode 100644 index 0000000000..cb2ec7c8f6 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/sell/di/SellCryptoComponentModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.onramp.sell.di + +import com.tangem.features.onramp.component.SellCryptoComponent +import com.tangem.features.onramp.sell.DefaultSellCryptoComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface SellCryptoComponentModule { + + @Binds + @Singleton + fun bindSellCryptoComponentFactory(factory: DefaultSellCryptoComponent.Factory): SellCryptoComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/ExchangeCardUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/ExchangeCardUM.kt new file mode 100644 index 0000000000..07c60222b4 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/ExchangeCardUM.kt @@ -0,0 +1,63 @@ +package com.tangem.features.onramp.swap.entity + +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.onramp.impl.R + +/** + * Exchange card UI model + * +[REDACTED_AUTHOR] + */ +internal sealed interface ExchangeCardUM { + + /** Title reference */ + val titleReference: TextReference + + /** Flag that indicates if remove button should be shown */ + val hasRemoveButton: Boolean + + /** Token item state */ + val tokenItemState: TokenItemState + + /** + * Empty state + * + * @property titleReference title reference + * @property onItemClick callback which will be called when an item is clicked + */ + data class Empty( + override val titleReference: TextReference, + val onItemClick: () -> Unit, + ) : ExchangeCardUM { + + override val hasRemoveButton: Boolean = false + + // TODO: [REDACTED_JIRA] + override val tokenItemState: TokenItemState = TokenItemState.Content( + id = "empty", + iconState = CurrencyIconState.Empty(R.drawable.ic_empty_64), + titleState = TokenItemState.TitleState.Content(text = "Choose the Token"), + subtitleState = TokenItemState.SubtitleState.TextContent(value = "You want to Swap"), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = ""), + subtitle2State = TokenItemState.Subtitle2State.TextContent(text = ""), + onItemClick = onItemClick, + onItemLongClick = null, + ) + } + + /** + * Filled + * + * @property titleReference title reference + * @property tokenItemState token item state + */ + data class Filled( + override val titleReference: TextReference, + override val tokenItemState: TokenItemState, + ) : ExchangeCardUM { + + override val hasRemoveButton: Boolean = true + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt new file mode 100644 index 0000000000..4b1ce4c245 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt @@ -0,0 +1,124 @@ +package com.tangem.features.onramp.swap.ui + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.rows.NetworkTitle +import com.tangem.core.ui.components.token.TokenItem +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.swap.entity.ExchangeCardUM + +/** + * Exchange card + * + * @param state state + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@Composable +internal fun ExchangeCard(state: ExchangeCardUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxWidth() + .heightIn(min = 116.dp) + .background( + color = TangemTheme.colors.background.primary, + shape = TangemTheme.shapes.roundedCornersXMedium, + ), + verticalArrangement = Arrangement.SpaceBetween, + ) { + NetworkTitle( + title = { + Text( + text = state.titleReference.resolveReference(), + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography.subtitle2, + ) + }, + action = { + RemoveButton(hasRemoveButton = state.hasRemoveButton, onClick = {}) + }, + ) + + TokenItem(state = state.tokenItemState, isBalanceHidden = false) + } +} + +@Composable +private fun RemoveButton(hasRemoveButton: Boolean, onClick: () -> Unit) { + AnimatedVisibility(visible = hasRemoveButton) { + Text( + text = stringResource(id = R.string.manage_tokens_remove), + modifier = Modifier.clickable( + indication = ripple(bounded = false), + interactionSource = remember { MutableInteractionSource() }, + onClick = onClick, + ), + color = TangemTheme.colors.text.accent, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography.body2, + ) + } +} + +@Preview +@Composable +private fun Preview_ExchangeCard(@PreviewParameter(ExchangeCardUMProvider::class) state: ExchangeCardUM) { + TangemThemePreview { + ExchangeCard(state = state) + } +} + +private class ExchangeCardUMProvider : PreviewParameterProvider { + + override val values: Sequence = sequenceOf( + ExchangeCardUM.Empty( + titleReference = resourceReference(id = R.string.swapping_from_title), + onItemClick = {}, + ), + ExchangeCardUM.Filled( + titleReference = resourceReference(id = R.string.swapping_from_title), + tokenItemState = TokenItemState.Content( + id = "1", + iconState = CurrencyIconState.Locked, + titleState = TokenItemState.TitleState.Content(text = "Bitcoin"), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"), + subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "0,35853044 BTC"), + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( + price = "34 496,75 \$", + priceChangePercent = "0,43 %", + type = PriceChangeType.DOWN, + ), + onItemClick = {}, + onItemLongClick = {}, + ), + ), + ) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt new file mode 100644 index 0000000000..67ada9b94b --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt @@ -0,0 +1,39 @@ +package com.tangem.features.onramp.tokenlist + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.onramp.tokenlist.model.OnrampTokenListModel +import com.tangem.features.onramp.tokenlist.ui.TokenList +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Stable +internal class DefaultOnrampTokenListComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: OnrampTokenListComponent.Params, +) : OnrampTokenListComponent, AppComponentContext by context { + + private val model: OnrampTokenListModel = getOrCreateModel(params) + + @Composable + override fun Content(contentPadding: PaddingValues, modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + + TokenList(state = state, contentPadding = contentPadding, modifier = modifier) + } + + @AssistedFactory + interface Factory : OnrampTokenListComponent.Factory { + override fun create( + context: AppComponentContext, + params: OnrampTokenListComponent.Params, + ): DefaultOnrampTokenListComponent + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt new file mode 100644 index 0000000000..bcb5ab848a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt @@ -0,0 +1,36 @@ +package com.tangem.features.onramp.tokenlist + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.onramp.entity.OnrampOperation + +/** Token list component that present list of token for multi-currency wallet */ +@Stable +internal interface OnrampTokenListComponent { + + @Composable + fun Content(contentPadding: PaddingValues, modifier: Modifier) + + /** Component factory */ + interface Factory : ComponentFactory + + /** + * Params + * + * @property filterOperation operation that is used to filter tokens by availability + * @property hasSearchBar flag that indicates if search bar should be shown + * @property userWalletId id of multi-currency wallet + * @property onTokenClick callback for token click + */ + data class Params( + val filterOperation: OnrampOperation, + val hasSearchBar: Boolean, + val userWalletId: UserWalletId, + val onTokenClick: (CryptoCurrencyStatus) -> Unit, + ) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/di/OnrampTokenListComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/di/OnrampTokenListComponentModule.kt new file mode 100644 index 0000000000..913817a3e6 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/di/OnrampTokenListComponentModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.tokenlist.di + +import com.tangem.features.onramp.tokenlist.DefaultOnrampTokenListComponent +import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface OnrampTokenListComponentModule { + + @Binds + @Singleton + fun bindOnrampTokenListComponentFactory( + factory: DefaultOnrampTokenListComponent.Factory, + ): OnrampTokenListComponent.Factory +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/di/OnrampTokenListModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/di/OnrampTokenListModelModule.kt new file mode 100644 index 0000000000..7977384f55 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/di/OnrampTokenListModelModule.kt @@ -0,0 +1,20 @@ +package com.tangem.features.onramp.tokenlist.di + +import com.tangem.core.decompose.di.DecomposeComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.onramp.tokenlist.model.OnrampTokenListModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(DecomposeComponent::class) +internal interface OnrampTokenListModelModule { + + @Binds + @IntoMap + @ClassKey(OnrampTokenListModel::class) + fun bindOnrampTokenListModel(model: OnrampTokenListModel): Model +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt new file mode 100644 index 0000000000..bb9e8f3562 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt @@ -0,0 +1,35 @@ +package com.tangem.features.onramp.tokenlist.entity + +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * Token list UM + * + * @property items items (search bar, tokens, headers) + * @property isBalanceHidden flag that indicates if balance should be hidden + * +[REDACTED_AUTHOR] + */ +internal data class TokenListUM( + val items: ImmutableList, + val isBalanceHidden: Boolean, +) { + + /** Get search bar if it exists */ + fun getSearchBar(): TokensListItemUM.SearchBar? { + return items.firstOrNull() as? TokensListItemUM.SearchBar + } + + /** Get tokens */ + fun getTokens(): ImmutableList { + if (getSearchBar() == null) return items + + return if (items.size > 1) { + items.subList(fromIndex = 1, toIndex = items.size) + } else { + persistentListOf() + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt new file mode 100644 index 0000000000..bb662a5b7a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt @@ -0,0 +1,41 @@ +package com.tangem.features.onramp.tokenlist.entity + +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import timber.log.Timber +import javax.inject.Inject + +/** + * [TokenListUM] controller + * +[REDACTED_AUTHOR] + */ +internal class TokenListUMController @Inject constructor() { + + val state: StateFlow get() = _state + + private val _state: MutableStateFlow = MutableStateFlow( + value = TokenListUM( + items = persistentListOf(), + isBalanceHidden = false, + ), + ) + + fun update(transform: (TokenListUM) -> TokenListUM) { + Timber.d("Applying non-name transformation") + _state.update(transform) + } + + fun update(transformer: TokenListUMTransformer) { + Timber.d("Applying ${transformer::class.simpleName}") + _state.update(transformer::transform) + } + + /** Get search bar if it exists */ + fun getSearchBar(): TokensListItemUM.SearchBar? { + return _state.value.getSearchBar() + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMTransformer.kt new file mode 100644 index 0000000000..4ffbf26c31 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMTransformer.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onramp.tokenlist.entity + +import com.tangem.utils.transformer.Transformer + +/** + * Base [TokenListUM] transformer + * +[REDACTED_AUTHOR] + */ +internal interface TokenListUMTransformer : Transformer \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SearchBarUMTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SearchBarUMTransformer.kt new file mode 100644 index 0000000000..db17cb19ea --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SearchBarUMTransformer.kt @@ -0,0 +1,33 @@ +package com.tangem.features.onramp.tokenlist.entity.transformer + +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer +import kotlinx.collections.immutable.persistentListOf + +/** + * Base [SearchBarUM] transformer + * +[REDACTED_AUTHOR] + */ +internal abstract class SearchBarUMTransformer : TokenListUMTransformer { + + abstract fun transform(prevState: SearchBarUM): SearchBarUM + + override fun transform(prevState: TokenListUM): TokenListUM { + val searchBarItem = prevState.getSearchBar() + + return if (searchBarItem != null) { + val updatedSearchBar = searchBarItem.copy(searchBarUM = transform(searchBarItem.searchBarUM)) + + prevState.copy( + items = persistentListOf( + updatedSearchBar, + *prevState.getTokens().toTypedArray(), + ), + ) + } else { + prevState + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt new file mode 100644 index 0000000000..cf69073114 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchBarActiveStateTransformer.kt @@ -0,0 +1,19 @@ +package com.tangem.features.onramp.tokenlist.entity.transformer + +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onramp.impl.R + +internal class UpdateSearchBarActiveStateTransformer(private val isActive: Boolean) : SearchBarUMTransformer() { + + override fun transform(prevState: SearchBarUM): SearchBarUM { + val placeholderText = if (isActive) { + TextReference.EMPTY + } else { + resourceReference(id = R.string.common_search) + } + + return prevState.copy(placeholderText = placeholderText, isActive = isActive) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt new file mode 100644 index 0000000000..3e463d159a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateSearchQueryTransformer.kt @@ -0,0 +1,10 @@ +package com.tangem.features.onramp.tokenlist.entity.transformer + +import com.tangem.core.ui.components.fields.entity.SearchBarUM + +internal class UpdateSearchQueryTransformer(private val newQuery: String) : SearchBarUMTransformer() { + + override fun transform(prevState: SearchBarUM): SearchBarUM { + return prevState.copy(query = newQuery) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt new file mode 100644 index 0000000000..b6065912be --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt @@ -0,0 +1,48 @@ +package com.tangem.features.onramp.tokenlist.entity.transformer + +import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer +import kotlinx.collections.immutable.toImmutableList + +internal class UpdateTokenItemsTransformer( + private val tokenItemStateConverter: TokenItemStateConverter, + private val statuses: List, + private val isBalanceHidden: Boolean, + private val hasSearchBar: Boolean, + private val onQueryChange: (String) -> Unit, + private val onActiveChange: (Boolean) -> Unit, +) : TokenListUMTransformer { + + override fun transform(prevState: TokenListUM): TokenListUM { + val items = tokenItemStateConverter.convertList(input = statuses).map(TokensListItemUM::Token) + + val searchBarItem = if (hasSearchBar) { + prevState.getSearchBar() ?: createSearchBarItem() + } else { + null + } + + return prevState.copy( + items = (listOfNotNull(searchBarItem) + items).toImmutableList(), + isBalanceHidden = isBalanceHidden, + ) + } + + private fun createSearchBarItem(): TokensListItemUM.SearchBar { + return TokensListItemUM.SearchBar( + searchBarUM = SearchBarUM( + placeholderText = resourceReference(id = R.string.common_search), + query = "", + onQueryChange = onQueryChange, + isActive = false, + onActiveChange = onActiveChange, + ), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt new file mode 100644 index 0000000000..fc6329296f --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -0,0 +1,119 @@ +package com.tangem.features.onramp.tokenlist.model + +import arrow.core.getOrElse +import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.core.utils.getOrElse +import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.features.onramp.entity.OnrampOperation +import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMController +import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateSearchBarActiveStateTransformer +import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateSearchQueryTransformer +import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer +import com.tangem.features.onramp.tokenlist.utils.SearchTokensManager +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +internal class OnrampTokenListModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val tokenListUMController: TokenListUMController, + private val searchTokensManager: SearchTokensManager, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + private val getTokenListUseCase: GetTokenListUseCase, + private val getWalletsUseCase: GetWalletsUseCase, + private val rampStateManager: RampStateManager, +) : Model() { + + val state: StateFlow = tokenListUMController.state + + private val params: OnrampTokenListComponent.Params = paramsContainer.require() + private val scanResponse by lazy { + getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }.scanResponse + } + + init { + subscribeOnUpdateState() + } + + private fun subscribeOnUpdateState() { + combine( + flow = getTokenListUseCase.launch(userWalletId = params.userWalletId).distinctUntilChanged(), + flow2 = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(), + flow3 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(), + flow4 = searchTokensManager.query, + ) { maybeTokenList, appCurrency, isBalanceHidden, query -> + val currencies = maybeTokenList.getOrElse( + ifLoading = { it ?: TokenList.Empty }, + ifError = { TokenList.Empty }, + ) + .flattenCurrencies() + + val filterTokenList = currencies + .filterByQuery(query = query) + .filterByAvailability() + + UpdateTokenItemsTransformer( + tokenItemStateConverter = TokenItemStateConverter( + appCurrency = appCurrency, + onItemClick = params.onTokenClick, + ), + statuses = filterTokenList, + isBalanceHidden = isBalanceHidden, + hasSearchBar = params.hasSearchBar && currencies.isNotEmpty(), + onQueryChange = ::onSearchQueryChange, + onActiveChange = ::onSearchBarActiveChange, + ) + } + .onEach(tokenListUMController::update) + .flowOn(dispatchers.main) + .launchIn(modelScope) + } + + private fun onSearchQueryChange(newQuery: String) { + val searchBar = tokenListUMController.getSearchBar() + if (searchBar?.searchBarUM?.query == newQuery) return + + modelScope.launch { + tokenListUMController.update(transformer = UpdateSearchQueryTransformer(newQuery)) + + searchTokensManager.update(newQuery) + } + } + + private fun onSearchBarActiveChange(isActive: Boolean) { + tokenListUMController.update(transformer = UpdateSearchBarActiveStateTransformer(isActive)) + } + + private fun List.filterByQuery(query: String): List { + return filter { + it.currency.name.contains(other = query, ignoreCase = true) || + it.currency.symbol.contains(other = query, ignoreCase = true) + } + } + + private fun List.filterByAvailability(): List { + return filter { + when (params.filterOperation) { + OnrampOperation.BUY -> { + rampStateManager.availableForBuy(scanResponse = scanResponse, cryptoCurrency = it.currency) + } + OnrampOperation.SELL -> rampStateManager.availableForSell(cryptoCurrency = it.currency) + } + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt new file mode 100644 index 0000000000..c75b080c3b --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -0,0 +1,65 @@ +package com.tangem.features.onramp.tokenlist.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.tokenlist.TokenListItem +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider + +/** + * Token list + * + * @param state state + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +@Composable +internal fun TokenList(state: TokenListUM, contentPadding: PaddingValues, modifier: Modifier = Modifier) { + LazyColumn(modifier = modifier, contentPadding = contentPadding) { + itemsIndexed( + items = state.items, + key = { _, item -> item.id }, + contentType = { _, item -> item::class.java }, + itemContent = { index, item -> + TokenListItem( + state = item, + isBalanceHidden = state.isBalanceHidden, + modifier = Modifier + .animateItem() + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = state.items.lastIndex, + addDefaultPadding = false, + backgroundColor = TangemTheme.colors.background.primary, + ), + ) + }, + ) + } +} + +@Preview +@Composable +private fun Preview_TokenList(@PreviewParameter(PreviewTokenListUMProvider::class) state: TokenListUM) { + TangemThemePreview { + TokenList( + state = state, + contentPadding = PaddingValues(all = 16.dp), + modifier = Modifier + .fillMaxWidth() + .background(color = TangemTheme.colors.background.secondary), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt new file mode 100644 index 0000000000..ce2f3e4f88 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt @@ -0,0 +1,62 @@ +package com.tangem.features.onramp.tokenlist.ui.preview + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import kotlinx.collections.immutable.toImmutableList + +internal class PreviewTokenListUMProvider : PreviewParameterProvider { + + override val values: Sequence = sequenceOf( + createTokensList(hasSearchBar = false, createDefaultTokenItem()), + createTokensList(hasSearchBar = true, createDefaultTokenItem()), + ) + + private fun createTokensList(hasSearchBar: Boolean, vararg items: TokensListItemUM): TokenListUM { + return TokenListUM( + items = buildList { + if (hasSearchBar) { + TokensListItemUM.SearchBar( + searchBarUM = SearchBarUM( + placeholderText = resourceReference(id = R.string.common_search), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = {}, + ), + ) + .let { add(element = it) } + } + + addAll(items) + } + .toImmutableList(), + isBalanceHidden = false, + ) + } + + private fun createDefaultTokenItem(): TokensListItemUM.Token { + return TokensListItemUM.Token( + state = TokenItemState.Content( + id = "1", + iconState = CurrencyIconState.Locked, + titleState = TokenItemState.TitleState.Content(text = "Bitcoin"), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"), + subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "0,35853044 BTC"), + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( + price = "34 496,75 \$", + priceChangePercent = "0,43 %", + type = PriceChangeType.DOWN, + ), + onItemClick = {}, + onItemLongClick = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/utils/SearchTokensManager.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/utils/SearchTokensManager.kt new file mode 100644 index 0000000000..dde1a1ab4f --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/utils/SearchTokensManager.kt @@ -0,0 +1,33 @@ +package com.tangem.features.onramp.tokenlist.utils + +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.withDebounce +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + +/** + * Search tokens manager + * +[REDACTED_AUTHOR] + */ +internal class SearchTokensManager @Inject constructor() { + + val query: Flow + get() = _query + + private val _query = MutableStateFlow(value = "") + + private val jobHolder = JobHolder() + + suspend fun update(value: String) { + coroutineScope { + if (value.isEmpty()) { + _query.value = value + } else { + withDebounce(jobHolder) { _query.value = value } + } + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/ui/ResidenceBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/ui/ResidenceBottomSheet.kt new file mode 100644 index 0000000000..a950da23ab --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/ui/ResidenceBottomSheet.kt @@ -0,0 +1,165 @@ +package com.tangem.features.onramp.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SecondaryButton +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.onramp.component.ConfirmResidencyComponent +import com.tangem.features.onramp.component.preview.PreviewConfirmResidencyComponent +import com.tangem.features.onramp.entity.ConfirmResidencyUM +import com.tangem.features.onramp.impl.R + +@Composable +internal fun ConfirmResidencyBottomSheet(config: TangemBottomSheetConfig, content: @Composable (Modifier) -> Unit) { + TangemBottomSheet( + config = config, + addBottomInsets = true, + containerColor = TangemTheme.colors.background.tertiary, + title = { _ -> Title() }, + content = { + val contentModifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .padding(bottom = TangemTheme.dimens.spacing16) + .fillMaxWidth() + .wrapContentHeight() + + content(contentModifier) + }, + ) +} + +@Composable +private fun Title(modifier: Modifier = Modifier) { + TangemTopAppBar( + modifier = modifier, + title = resourceReference(R.string.onramp_residency_bottomsheet_title), + titleAlignment = Alignment.CenterHorizontally, + height = TangemTopAppBarHeight.BOTTOM_SHEET, + ) +} + +@Composable +internal fun ConfirmResidencyBottomSheetContent(model: ConfirmResidencyUM, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + CountryContent( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = TangemTheme.dimens.spacing24), + name = model.country, + flagUrl = model.countryFlagUrl, + isCountrySupported = model.isCountrySupported, + ) + SecondaryButton( + modifier = Modifier.fillMaxWidth(), + text = model.secondaryButtonConfig.text.resolveReference(), + onClick = model.secondaryButtonConfig.onClick, + ) + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(top = TangemTheme.dimens.spacing12), + text = model.primaryButtonConfig.text.resolveReference(), + onClick = model.primaryButtonConfig.onClick, + ) + } +} + +@Composable +private fun CountryContent(name: String, flagUrl: String, isCountrySupported: Boolean, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val flagSize = with(LocalDensity.current) { TangemTheme.dimens.size36.roundToPx() } + SubcomposeAsyncImage( + modifier = Modifier.size(TangemTheme.dimens.size36), + model = ImageRequest.Builder(LocalContext.current) + .size(size = flagSize) + .data(flagUrl) + .memoryCacheKey(flagUrl + flagSize) + .crossfade(true) + .allowHardware(false) + .build(), + loading = { CircleShimmer() }, + contentDescription = null, + ) + + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12), + text = name, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + maxLines = 1, + ) + + Text( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing6), + text = stringResource( + id = if (isCountrySupported) { + R.string.onramp_residency_bottomsheet_country_subtitle + } else { + R.string.onramp_residency_bottomsheet_country_not_supported + }, + ), + color = if (isCountrySupported) TangemTheme.colors.text.tertiary else TangemTheme.colors.text.warning, + style = TangemTheme.typography.body2, + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun Preview_ConfirmResidencyBottomSheet( + @PreviewParameter(ConfirmResidencyComponentPreviewProvider::class) component: ConfirmResidencyComponent, +) { + TangemThemePreview { + component.BottomSheet() + } +} + +private class ConfirmResidencyComponentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + PreviewConfirmResidencyComponent(), + PreviewConfirmResidencyComponent( + initialState = ConfirmResidencyUM( + country = "Russia", + countryFlagUrl = "https://hatscripts.github.io/circle-flags/flags/ru.svg", + isCountrySupported = false, + primaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig( + onClick = {}, + text = stringReference("Close"), + ), + secondaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig( + onClick = {}, + text = stringReference("Change"), + ), + ), + ), + ) +} \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt index ddcc7ab29a..030e8e81c7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/SendStateFactory.kt @@ -2,10 +2,13 @@ package com.tangem.features.send.impl.presentation.state import com.tangem.blockchain.common.TransactionData import com.tangem.common.ui.amountScreen.converters.AmountStateConverter +import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter +import com.tangem.common.ui.amountScreen.models.AmountParameters import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet @@ -33,14 +36,15 @@ internal class SendStateFactory( private val isTapHelpPreviewEnabledProvider: Provider, ) { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) + private val maxEnterAmountConverter = MaxEnterAmountConverter() private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) { AmountStateConverter( clickIntents = clickIntents, appCurrencyProvider = appCurrencyProvider, iconStateConverter = iconStateConverter, - userWalletProvider = userWalletProvider, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatusProvider()), ) } private val recipientStateConverter by lazy(LazyThreadSafetyMode.NONE) { @@ -79,7 +83,12 @@ internal class SendStateFactory( fun getReadyState(): SendUiState { val state = currentStateProvider() val amountState = if (state.amountState is AmountState.Empty) { - amountStateConverter.convert("") + amountStateConverter.convert( + AmountParameters( + title = stringReference(userWalletProvider().name), + value = "", + ), + ) } else { state.amountState } @@ -96,7 +105,12 @@ internal class SendStateFactory( fun getReadyState(amount: String, destinationAddress: String, memo: String?): SendUiState { val state = currentStateProvider() val amountState = if (state.amountState is AmountState.Empty) { - amountStateConverter.convert(amount) + amountStateConverter.convert( + AmountParameters( + title = stringReference(userWalletProvider().name), + value = amount, + ), + ) } else { state.amountState } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt index 204918b99b..9b8d11fb41 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/AmountStateFactory.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.impl.presentation.state.amount import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter @@ -16,6 +17,7 @@ internal class AmountStateFactory( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + private val minimumTransactionAmountProvider: Provider, ) { private val amountFieldChangeConverter by lazy(LazyThreadSafetyMode.NONE) { @@ -23,6 +25,7 @@ internal class AmountStateFactory( stateRouterProvider = stateRouterProvider, currentStateProvider = currentStateProvider, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + minimumTransactionAmountProvider = minimumTransactionAmountProvider, ) } private val amountFieldMaxAmountConverter by lazy(LazyThreadSafetyMode.NONE) { @@ -30,6 +33,7 @@ internal class AmountStateFactory( stateRouterProvider = stateRouterProvider, currentStateProvider = currentStateProvider, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + minimumTransactionAmountProvider = minimumTransactionAmountProvider, ) } @@ -51,6 +55,7 @@ internal class AmountStateFactory( stateRouterProvider = stateRouterProvider, currentStateProvider = currentStateProvider, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + minimumTransactionAmountProvider = minimumTransactionAmountProvider, ) } private val amountReduceToConverter by lazy { @@ -58,6 +63,7 @@ internal class AmountStateFactory( stateRouterProvider = stateRouterProvider, currentStateProvider = currentStateProvider, cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + minimumTransactionAmountProvider = minimumTransactionAmountProvider, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt index 0468cd8c82..c0e28102e7 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceByConverter.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.impl.presentation.state.amount import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter @@ -11,6 +12,7 @@ internal class SendAmountReduceByConverter( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + private val minimumTransactionAmountProvider: Provider, ) : Converter { override fun convert(value: AmountReduceByTransformer.ReduceByData): SendUiState { @@ -23,7 +25,11 @@ internal class SendAmountReduceByConverter( sendState = state.sendState?.copy( reduceAmountBy = value.reduceAmountBy, ), - amountState = AmountReduceByTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState), + amountState = AmountReduceByTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + minimumTransactionAmount = minimumTransactionAmountProvider(), + value = value, + ).transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt index 1bb5518063..ea1caaf180 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/amount/SendAmountReduceToConverter.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.impl.presentation.state.amount import com.tangem.common.ui.amountScreen.converters.AmountReduceToTransformer +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter @@ -12,6 +13,7 @@ internal class SendAmountReduceToConverter( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + private val minimumTransactionAmountProvider: Provider, ) : Converter { override fun convert(value: BigDecimal): SendUiState { @@ -21,7 +23,11 @@ internal class SendAmountReduceToConverter( return state.copyWrapped( isEditState = isEditState, - amountState = AmountReduceToTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState), + amountState = AmountReduceToTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + minimumTransactionAmount = minimumTransactionAmountProvider(), + value = value, + ).transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 48ec5be5b1..9975712433 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -11,6 +11,7 @@ import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalance import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification import com.tangem.common.ui.notifications.NotificationsFactory.addFeeCoverageNotification import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addMinimumAmountErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications @@ -172,7 +173,7 @@ internal class SendNotificationFactory( ) }, ) - if (!BlockchainUtils.isCardano(currency.id.value)) { + if (!BlockchainUtils.isCardano(currency.network.id.value)) { addDustWarningNotification( dustValue = currencyCheck.dustValue, feeValue = feeValue, @@ -192,6 +193,11 @@ internal class SendNotificationFactory( cryptoCurrency = currency, isAccountFunded = currencyCheck.isAccountFunded, ) + addMinimumAmountErrorNotification( + minimumSendAmount = currencyCheck.minimumSendAmount, + sendingAmount = sendingAmount, + cryptoCurrency = currency, + ) } private suspend fun MutableList.addWarningNotifications( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt index 4b5dda431d..ea7e502aec 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldChangeConverter.kt @@ -1,6 +1,8 @@ package com.tangem.features.send.impl.presentation.state.fields +import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter @@ -11,17 +13,29 @@ internal class SendAmountFieldChangeConverter( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + private val minimumTransactionAmountProvider: Provider, ) : Converter { + private val maxEnterAmountConverter = MaxEnterAmountConverter() + override fun convert(value: String): SendUiState { val state = currentStateProvider() + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val isEditState = stateRouterProvider().isEditState - val amountState = state.getAmountState(isEditState) ?: return state + val amountState = state.getAmountState(isEditState) + + val maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatus) + val minimumTransactionAmount = minimumTransactionAmountProvider() return state.copyWrapped( isEditState = isEditState, sendState = state.sendState?.copy(reduceAmountBy = null), - amountState = AmountFieldChangeTransformer(cryptoCurrencyStatusProvider(), value).transform(amountState), + amountState = AmountFieldChangeTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxEnterAmount = maxEnterAmount, + minimumTransactionAmount = minimumTransactionAmount, + value = value, + ).transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt index 5577388040..882365d726 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldMaxAmountConverter.kt @@ -1,6 +1,8 @@ package com.tangem.features.send.impl.presentation.state.fields -import com.tangem.common.ui.amountScreen.converters.field.AmountFieldMaxAmountTransformer +import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.SendUiState import com.tangem.features.send.impl.presentation.state.StateRouter @@ -12,8 +14,11 @@ internal class SendAmountFieldMaxAmountConverter( private val stateRouterProvider: Provider, private val currentStateProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, + private val minimumTransactionAmountProvider: Provider, ) : Converter { + private val maxEnterAmountConverter = MaxEnterAmountConverter() + override fun convert(value: Unit): SendUiState { val state = currentStateProvider() val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() @@ -23,10 +28,17 @@ internal class SendAmountFieldMaxAmountConverter( val decimalCryptoValue = cryptoCurrencyStatus.value.amount if (decimalCryptoValue.isNullOrZero()) return state + val maxEnterAmount = maxEnterAmountConverter.convert(cryptoCurrencyStatus) + val minimumTransactionAmount = minimumTransactionAmountProvider() + return state.copyWrapped( isEditState = isEditState, sendState = state.sendState?.copy(reduceAmountBy = null), - amountState = AmountFieldMaxAmountTransformer(cryptoCurrencyStatusProvider()).transform(amountState), + amountState = AmountFieldSetMaxAmountTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxAmount = maxEnterAmount, + minAmount = minimumTransactionAmount, + ).transform(amountState), ) } } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientHistoryListConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientHistoryListConverter.kt index 8a8576775e..946bcbb17c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientHistoryListConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientHistoryListConverter.kt @@ -4,6 +4,8 @@ import com.tangem.common.extensions.isZero import com.tangem.core.ui.extensions.TextReference 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.format import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday import com.tangem.core.ui.utils.toTimeFormat @@ -17,7 +19,6 @@ import com.tangem.features.send.impl.presentation.state.recipient.utils.RECENT_K import com.tangem.features.send.impl.presentation.state.recipient.utils.emptyListState import com.tangem.utils.Provider import com.tangem.utils.converter.Converter -import com.tangem.utils.toFormattedCurrencyString import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList @@ -76,10 +77,7 @@ internal class SendRecipientHistoryListConverter( } private fun TxHistoryItem.getAmount(cryptoCurrency: CryptoCurrency): String { - return amount.toFormattedCurrencyString( - currency = cryptoCurrency.symbol, - decimals = cryptoCurrency.decimals, - ) + return amount.format { crypto(cryptoCurrency) } } private fun TxHistoryItem.extractTimestamp(): TextReference { diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt index 5c9e9b0679..5b59bbb297 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/recipient/SendRecipientMemoFieldConverter.kt @@ -38,6 +38,7 @@ internal class SendRecipientMemoFieldConverter( Blockchain.Algorand.id, Blockchain.Sei.id, Blockchain.InternetComputer.id, + Blockchain.Casper.id, -> { convert( value = Data(memo = memo, label = R.string.send_extras_hint_memo), diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt index 864a75afa0..4c59863b0c 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendNavigationButtons.kt @@ -32,6 +32,9 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.send.impl.presentation.state.SendStates @@ -228,12 +231,14 @@ private fun SendStates.FeeState.getFiatValue() = if (isFeeConvertibleToFiat) { ) } else { val amount = fee?.amount - BigDecimalFormatter.formatCryptoFeeAmount( - cryptoAmount = amount?.value, - cryptoCurrency = amount?.currencySymbol.orEmpty(), - decimals = amount?.decimals ?: 0, - canBeLower = isFeeApproximate, - ) + amount?.value.format { + crypto( + decimals = amount?.decimals ?: 0, + symbol = amount?.currencySymbol.orEmpty(), + ).fee( + canBeLower = isFeeApproximate, + ) + } } private fun getButtonData( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt index 3324d89836..c588753385 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/SendScreen.kt @@ -88,7 +88,7 @@ private fun SendAppBar(uiState: SendUiState, currentState: SendUiCurrentScreen) -> resourceReference(R.string.common_fee_selector_title) to null SendUiStateType.Send -> if (uiState.sendState?.isSuccess == false) { resourceReference(R.string.send_summary_title, wrappedList(uiState.cryptoCurrencyName)) to - (uiState.amountState as? AmountState.Data)?.walletName + (uiState.amountState as? AmountState.Data)?.title } else { null to null } @@ -108,7 +108,7 @@ private fun SendAppBar(uiState: SendUiState, currentState: SendUiCurrentScreen) } AppBarWithBackButtonAndIcon( text = titleRes?.resolveReference(), - subtitle = subtitleRes, + subtitle = subtitleRes?.resolveReference(), onBackClick = uiState.clickIntents::onCloseClick, onIconClick = uiState.clickIntents::onQrCodeScanClick, backIconRes = backIcon, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt index 712f915547..6774c2572d 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/fee/SendSpeedSelectorItem.kt @@ -16,6 +16,9 @@ import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseToBigDecimal @@ -53,12 +56,12 @@ internal fun SendSpeedSelectorItem( onSelect = onSelect, modifier = modifier, preDot = stringReference( - BigDecimalFormatter.formatCryptoFeeAmount( - cryptoAmount = amount?.value, - cryptoCurrency = amount?.currencySymbol.orEmpty(), - decimals = amount?.decimals ?: 0, - canBeLower = state.isFeeApproximate, - ), + amount?.value.format { + crypto( + symbol = amount?.currencySymbol.orEmpty(), + decimals = amount?.decimals ?: 0, + ).fee(canBeLower = state.isFeeApproximate) + }, ), postDot = if (state.isFeeConvertibleToFiat) { getFiatReference(amount?.value, state.rate, state.appCurrency) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt index b396811b81..4d5830190e 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/send/FeeBlock.kt @@ -18,6 +18,9 @@ import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter @@ -62,12 +65,12 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, o titleRes = title, iconRes = icon, preDot = stringReference( - BigDecimalFormatter.formatCryptoFeeAmount( - cryptoAmount = feeAmount?.value, - cryptoCurrency = feeAmount?.currencySymbol.orEmpty(), - decimals = feeAmount?.decimals ?: 0, - canBeLower = feeState.isFeeApproximate, - ), + feeAmount?.value.format { + crypto( + symbol = feeAmount?.currencySymbol.orEmpty(), + decimals = feeAmount?.decimals ?: 0, + ).fee(canBeLower = feeState.isFeeApproximate) + }, ), postDot = if (feeState.isFeeConvertibleToFiat) { getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency) diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 468482bb95..14b94f1d08 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -12,6 +12,7 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.parseBigDecimal @@ -59,6 +60,7 @@ import com.tangem.features.send.impl.presentation.state.recipient.RecipientSendF import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.coroutines.* +import com.tangem.utils.extensions.orZero import com.tangem.utils.extensions.stripZeroPlainString import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.* @@ -78,6 +80,7 @@ internal class SendViewModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getWalletsUseCase: GetWalletsUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, + private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase, private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, @@ -153,6 +156,7 @@ internal class SendViewModel @Inject constructor( stateRouterProvider = Provider { stateRouter }, currentStateProvider = Provider { uiState.value }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + minimumTransactionAmountProvider = Provider { minimumTransactionAmount }, ) private val feeStateFactory = FeeStateFactory( @@ -212,6 +216,7 @@ internal class SendViewModel @Inject constructor( private var isTapHelpPreviewEnabled: Boolean = false private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() private var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null + private var minimumTransactionAmount: EnterAmountBoundary? = null private var balanceJobHolder = JobHolder() private var balanceHidingJobHolder = JobHolder() @@ -295,6 +300,7 @@ internal class SendViewModel @Inject constructor( onDataLoaded( currencyStatus = cryptoCurrencyStatus, feeCurrencyStatus = getFeeCurrencyStatusSync(cryptoCurrencyStatus, isMultiCurrency), + minTransactionAmount = getMinimumTransactionAmount(cryptoCurrencyStatus), ) }, ifLeft = { showErrorAlert() }, @@ -336,6 +342,18 @@ internal class SendViewModel @Inject constructor( } } + private suspend fun getMinimumTransactionAmount(cryptoCurrencyStatus: CryptoCurrencyStatus): EnterAmountBoundary? { + return getMinimumTransactionAmountSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ).getOrNull()?.let { + EnterAmountBoundary( + amount = it, + fiatRate = cryptoCurrencyStatus.value.fiatRate.orZero(), + ) + } + } + private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase() .map { maybeAppCurrency -> @@ -348,9 +366,14 @@ internal class SendViewModel @Inject constructor( ) } - private fun onDataLoaded(currencyStatus: CryptoCurrencyStatus, feeCurrencyStatus: CryptoCurrencyStatus?) { + private fun onDataLoaded( + currencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus?, + minTransactionAmount: EnterAmountBoundary?, + ) { cryptoCurrencyStatus = currencyStatus feeCryptoCurrencyStatus = feeCurrencyStatus + minimumTransactionAmount = minTransactionAmount subscribeOnQRScannerResult() when { uiState.value.sendState?.isSuccess == true -> return diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt index c2ddc2a16f..5decf65514 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt @@ -18,7 +18,7 @@ internal class StakingAnalyticSender( fun initialInfoScreen(value: StakingUiState) { val initialInfoState = value.initialInfoState as? StakingStates.InitialInfoState.Data val validatorState = initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data - val validatorCount = validatorState?.balance + val validatorCount = validatorState?.balances ?.filterNot { it.validator?.address.isNullOrBlank() } ?.distinctBy { it.validator?.address } ?.size ?: 0 @@ -32,10 +32,10 @@ internal class StakingAnalyticSender( fun confirmationScreen(value: StakingUiState) { val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data - val validatorState = confirmationState?.validatorState as? ValidatorState.Content + val validatorState = value.validatorState as? StakingStates.ValidatorState.Data val validatorName = validatorState?.chosenValidator?.name ?: return - if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) return + if (confirmationState?.innerState == InnerConfirmationStakingState.COMPLETED) return analyticsEventHandler.send( StakingAnalyticsEvent.ConfirmationScreenOpened( @@ -53,6 +53,7 @@ internal class StakingAnalyticSender( StakingStep.Amount -> StakeScreenSource.Amount StakingStep.Confirmation -> StakeScreenSource.Confirmation StakingStep.Validators, + StakingStep.RestakeValidator, StakingStep.RewardsValidators, -> StakeScreenSource.Validators }, @@ -75,8 +76,7 @@ internal class StakingAnalyticSender( } fun sendTransactionStakingAnalytics(value: StakingUiState) { - val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data - val validatorState = confirmationState?.validatorState as? ValidatorState.Content + val validatorState = value.validatorState as? StakingStates.ValidatorState.Data val validatorName = validatorState?.chosenValidator?.name ?: return analyticsEventHandler.send( @@ -98,8 +98,7 @@ internal class StakingAnalyticSender( } fun sendTransactionStakingClickedAnalytics(value: StakingUiState) { - val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data - val validatorState = confirmationState?.validatorState as? ValidatorState.Content + val validatorState = value.validatorState as? StakingStates.ValidatorState.Data val validatorName = validatorState?.chosenValidator?.name ?: return analyticsEventHandler.send( @@ -114,11 +113,9 @@ internal class StakingAnalyticSender( val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data return when (value.actionType) { - StakingActionCommonType.ENTER -> StakingActionType.STAKE - StakingActionCommonType.EXIT -> StakingActionType.UNSTAKE - StakingActionCommonType.PENDING_REWARDS, - StakingActionCommonType.PENDING_OTHER, - -> confirmationState?.pendingAction?.type ?: StakingActionType.UNKNOWN + StakingActionCommonType.Enter -> StakingActionType.STAKE + StakingActionCommonType.Exit -> StakingActionType.UNSTAKE + is StakingActionCommonType.Pending -> confirmationState?.pendingAction?.type ?: StakingActionType.UNKNOWN } } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt index b6e5bef985..94cd29f646 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt @@ -16,7 +16,7 @@ internal sealed class InnerYieldBalanceState { val rewardsFiat: String, val rewardBlockType: RewardBlockType, val isActionable: Boolean, - val balance: ImmutableList, + val balances: ImmutableList, ) : InnerYieldBalanceState() data object Empty : InnerYieldBalanceState() @@ -30,9 +30,10 @@ internal data class BalanceState( val subtitle: TextReference?, val isClickable: Boolean, val cryptoValue: String, - val cryptoDecimal: BigDecimal, - val cryptoAmount: TextReference, - val fiatAmount: TextReference, + val cryptoAmount: BigDecimal, + val formattedCryptoAmount: TextReference, + val fiatAmount: BigDecimal?, + val formattedFiatAmount: TextReference, val rawCurrencyId: String?, val validator: Yield.Validator?, val pendingActions: ImmutableList, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt index 8472172758..d9fd203280 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt @@ -39,7 +39,7 @@ internal object StakingNotification { ) : NotificationUM.Warning( title = title, subtitle = subtitle, - iconResId = R.drawable.ic_alert_circle_24, + iconResId = R.drawable.img_attention_20, buttonsState = buttonsState, onCloseClick = onCloseClick, ) { @@ -47,6 +47,11 @@ internal object StakingNotification { val title: TextReference, val description: TextReference, ) : StakingNotification.Warning(title = title, subtitle = description) + + data object LowStakedBalance : StakingNotification.Warning( + title = resourceReference(R.string.staking_notification_low_staked_balance_title), + subtitle = resourceReference(R.string.staking_notification_low_staked_balance_text), + ) } sealed class Info( @@ -59,7 +64,6 @@ internal object StakingNotification { subtitle = subtitle, buttonsState = buttonsState, onCloseClick = onCloseClick, - ) { data class EarnRewards( val subtitleText: TextReference, @@ -68,6 +72,11 @@ internal object StakingNotification { subtitle = subtitleText, ) + data object StakeEntireBalance : StakingNotification.Info( + title = resourceReference(R.string.common_network_fee_title), + subtitle = resourceReference(R.string.staking_notification_stake_entire_balance_text), + ) + data class Unstake( val cooldownPeriodDays: Int, @StringRes val subtitleRes: Int, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index 0b04c60e11..4150cef942 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -45,6 +45,12 @@ internal class StakingStateController @Inject constructor( mutableUiState.update(function = titleTransformer::transform) } + fun updateAll(vararg transformer: Transformer) { + transformer.forEach { mutableUiState.update(function = it::transform) } + mutableUiState.update(function = buttonsTransformer::transform) + mutableUiState.update(function = titleTransformer::transform) + } + fun clear() { mutableUiState.update { getInitialState() } mutableUiState.update(function = buttonsTransformer::transform) @@ -69,16 +75,19 @@ internal class StakingStateController @Inject constructor( walletName = "", cryptoCurrencyName = "", cryptoCurrencySymbol = "", + cryptoCurrencyBlockchainId = "", currentStep = StakingStep.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), amountState = AmountState.Empty(), + validatorState = StakingStates.ValidatorState.Empty(), rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(), confirmationState = StakingStates.ConfirmationState.Empty(), isBalanceHidden = false, event = consumedEvent(), bottomSheetConfig = null, - actionType = StakingActionCommonType.ENTER, + actionType = StakingActionCommonType.Enter, buttonsState = NavigationButtonsState.Empty, + balanceState = null, ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 64167ead85..d7e8d24c25 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -5,6 +5,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.analytics.StakingAnalyticsEvent import com.tangem.features.staking.impl.analytics.utils.StakingAnalyticSender +import com.tangem.lib.crypto.BlockchainUtils.isSolana internal class StakingStateRouter( private val appRouter: AppRouter, @@ -23,12 +24,19 @@ internal class StakingStateRouter( fun onNextClick() { when (stateController.value.currentStep) { StakingStep.InitialInfo -> when (stateController.value.actionType) { - StakingActionCommonType.ENTER -> showAmount() - StakingActionCommonType.PENDING_OTHER, - StakingActionCommonType.EXIT, + StakingActionCommonType.Enter -> showAmount() + // TODO staking [REDACTED_TASK_KEY] support solana multisize hashes signing + StakingActionCommonType.Exit -> if (isSolana(stateController.value.cryptoCurrencyBlockchainId)) { + showConfirmation() + } else { + showAmount() + } + StakingActionCommonType.Pending.Other, + StakingActionCommonType.Pending.Rewards, -> showConfirmation() - StakingActionCommonType.PENDING_REWARDS -> showRewardsValidators() + StakingActionCommonType.Pending.Restake -> showRestakeValidators() } + StakingStep.RestakeValidator, StakingStep.RewardsValidators, StakingStep.Validators, StakingStep.Amount, @@ -41,25 +49,36 @@ internal class StakingStateRouter( val uiState = stateController.uiState.value when (uiState.currentStep) { StakingStep.InitialInfo -> onBackClick() - StakingStep.Amount -> showInitial() + StakingStep.RestakeValidator, + StakingStep.RewardsValidators, + StakingStep.Amount, + -> showInitial() StakingStep.Confirmation -> { - if (uiState.actionType != StakingActionCommonType.ENTER) { - showInitial() - } else { + val isEnter = uiState.actionType == StakingActionCommonType.Enter + val isExit = uiState.actionType == StakingActionCommonType.Exit + + // TODO staking [REDACTED_TASK_KEY] support solana multisize hashes signing + val isSolana = isSolana(uiState.cryptoCurrencyBlockchainId) + if (isEnter || isExit && !isSolana) { showAmount() + } else { + showInitial() } } StakingStep.Validators -> showConfirmation() - StakingStep.RewardsValidators -> showInitial() } } + fun showValidators() { + stateController.update { it.copy(currentStep = StakingStep.Validators) } + } + private fun showInitial() { analyticSender.initialInfoScreen(stateController.value) stateController.update { it.copy(currentStep = StakingStep.InitialInfo) } } - private fun showRewardsValidators() { + fun showRewardsValidators() { analyticsEventsHandler.send(StakingAnalyticsEvent.RewardScreenOpened) stateController.update { it.copy(currentStep = StakingStep.RewardsValidators) } } @@ -69,8 +88,8 @@ internal class StakingStateRouter( stateController.update { it.copy(currentStep = StakingStep.Amount) } } - fun showValidators() { - stateController.update { it.copy(currentStep = StakingStep.Validators) } + private fun showRestakeValidators() { + stateController.update { it.copy(currentStep = StakingStep.RestakeValidator) } } private fun showConfirmation() { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 4bc8b84bda..c8c0be08e6 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -9,8 +9,8 @@ import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig -import com.tangem.domain.staking.model.PendingTransaction import com.tangem.domain.staking.model.stakekit.PendingAction +import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingEvent @@ -29,26 +29,31 @@ internal data class StakingUiState( val walletName: String, val cryptoCurrencyName: String, val cryptoCurrencySymbol: String, + val cryptoCurrencyBlockchainId: String, val currentStep: StakingStep, val initialInfoState: StakingStates.InitialInfoState, val amountState: AmountState, val rewardsValidatorsState: StakingStates.RewardsValidatorsState, val confirmationState: StakingStates.ConfirmationState, + val validatorState: StakingStates.ValidatorState, val isBalanceHidden: Boolean, val bottomSheetConfig: TangemBottomSheetConfig?, val actionType: StakingActionCommonType, val buttonsState: NavigationButtonsState, val event: StateEvent, + val balanceState: BalanceState?, ) { fun copyWrapped( initialInfoState: StakingStates.InitialInfoState = this.initialInfoState, amountState: AmountState = this.amountState, confirmationState: StakingStates.ConfirmationState = this.confirmationState, + validatorState: StakingStates.ValidatorState = this.validatorState, ): StakingUiState = copy( initialInfoState = initialInfoState, amountState = amountState, confirmationState = confirmationState, + validatorState = validatorState, ) } @@ -85,21 +90,38 @@ internal sealed class StakingStates { ) : RewardsValidatorsState() } + sealed class ValidatorState : StakingStates() { + abstract val isClickable: Boolean + + data class Data( + override val isPrimaryButtonEnabled: Boolean, + override val isClickable: Boolean, + val isVisibleOnConfirmation: Boolean, + val chosenValidator: Yield.Validator, + val activeValidator: Yield.Validator?, + val availableValidators: List, + ) : ValidatorState() + + data class Empty( + override val isClickable: Boolean = false, + override val isPrimaryButtonEnabled: Boolean = false, + ) : ValidatorState() + } + /** Confirmation state */ sealed class ConfirmationState : StakingStates() { data class Data( override val isPrimaryButtonEnabled: Boolean, val innerState: InnerConfirmationStakingState, val feeState: FeeState, - val validatorState: ValidatorState, val pendingAction: PendingAction?, val pendingActions: ImmutableList?, val notifications: ImmutableList, val footerText: TextReference, val transactionDoneState: TransactionDoneState, val isApprovalNeeded: Boolean, + val allowance: BigDecimal, val reduceAmountBy: BigDecimal?, - val possiblePendingTransaction: PendingTransaction?, ) : ConfirmationState() data class Empty( @@ -112,6 +134,7 @@ enum class StakingStep { InitialInfo, RewardsValidators, Amount, + RestakeValidator, Confirmation, Validators, } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt deleted file mode 100644 index 7871149443..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/ValidatorState.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.features.staking.impl.presentation.state - -import androidx.compose.runtime.Immutable -import com.tangem.domain.staking.model.stakekit.Yield - -@Immutable -internal sealed class ValidatorState { - - abstract val isClickable: Boolean - - data class Content( - override val isClickable: Boolean, - val chosenValidator: Yield.Validator, - val availableValidators: List, - ) : ValidatorState() - - data object Loading : ValidatorState() { - override val isClickable: Boolean - get() = false - } - - data object Error : ValidatorState() { - override val isClickable: Boolean - get() = false - } - - fun copySealed(isClickable: Boolean): ValidatorState { - return if (this is Content) { - copy(isClickable = isClickable) - } else { - this - } - } -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt index e08e0a26d1..8051fb9e54 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/BalanceItemConverter.kt @@ -1,6 +1,8 @@ package com.tangem.features.staking.impl.presentation.state.converters import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency @@ -45,14 +47,12 @@ internal class BalanceItemConverter( subtitle = getSubtitle(value), type = value.type, cryptoValue = cryptoAmount.parseBigDecimal(cryptoCurrency.decimals), - cryptoDecimal = cryptoAmount, - cryptoAmount = stringReference( - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = cryptoAmount, - cryptoCurrency = cryptoCurrency, - ), + cryptoAmount = cryptoAmount, + formattedCryptoAmount = stringReference( + cryptoAmount.format { crypto(cryptoCurrency) }, ), - fiatAmount = stringReference( + fiatAmount = fiatAmount, + formattedFiatAmount = stringReference( BigDecimalFormatter.formatFiatAmount( fiatAmount = fiatAmount, fiatCurrencyCode = appCurrency.code, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt index aca5b46db7..b151140d55 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/RewardsValidatorStateConverter.kt @@ -1,6 +1,8 @@ package com.tangem.features.staking.impl.presentation.state.converters import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency @@ -65,12 +67,11 @@ internal class RewardsValidatorStateConverter( val appCurrency = appCurrencyProvider() val cryptoCurrency = cryptoCurrencyStatus.currency val cryptoAmount = stringReference( - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = cryptoValue, - cryptoCurrency = cryptoCurrency, - ), + cryptoValue.format { + crypto(cryptoCurrency) + }, ) - val fiatAmount = stringReference( + val formattedFiatAmount = stringReference( BigDecimalFormatter.formatFiatAmount( fiatAmount = fiatValue, fiatCurrencyCode = appCurrency.code, @@ -84,9 +85,10 @@ internal class RewardsValidatorStateConverter( title = stringReference(this.name), subtitle = null, cryptoValue = cryptoValue.parseBigDecimal(cryptoCurrency.decimals), - cryptoDecimal = cryptoValue, - cryptoAmount = cryptoAmount, - fiatAmount = fiatAmount, + cryptoAmount = cryptoValue, + formattedCryptoAmount = cryptoAmount, + fiatAmount = fiatValue, + formattedFiatAmount = formattedFiatAmount, rawCurrencyId = cryptoCurrency.id.rawCurrencyId, pendingActions = balance.pendingActions.toPersistentList(), isClickable = true, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt index fc1f5aa7b3..41c9881df8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/YieldBalancesConverter.kt @@ -1,11 +1,14 @@ package com.tangem.features.staking.impl.presentation.state.converters import com.tangem.common.extensions.isZero -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState +import com.tangem.lib.crypto.BlockchainUtils.isBSC import com.tangem.lib.crypto.BlockchainUtils.isSolana import com.tangem.utils.Provider import com.tangem.utils.converter.Converter @@ -41,18 +44,16 @@ internal class YieldBalancesConverter( } val (type, isActionable) = getRewardBlockType() InnerYieldBalanceState.Data( - rewardsCrypto = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = cryptoRewardsValue, - cryptoCurrency = cryptoCurrency, - ), - rewardsFiat = BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatRewardsValue, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), + rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) }, + rewardsFiat = fiatRewardsValue.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, rewardBlockType = type, isActionable = isActionable, - balance = balanceToShowItems.mapBalances(), + balances = balanceToShowItems.mapBalances(), ) } else { InnerYieldBalanceState.Empty @@ -62,12 +63,13 @@ internal class YieldBalancesConverter( private fun List.mapBalances() = asSequence() .filterNot { it.amount.isZero() || it.type == BalanceType.REWARDS } .mapNotNull(balanceItemConverter::convert) - .sortedByDescending { it.cryptoDecimal } + .sortedByDescending { it.cryptoAmount } .sortedBy { it.type.order } .toPersistentList() private fun getRewardBlockType(): Pair { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val blockchainId = cryptoCurrencyStatus.currency.network.id.value val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data val rewards = yieldBalance?.balance?.items ?.filter { it.type == BalanceType.REWARDS && !it.amount.isZero() } @@ -75,10 +77,8 @@ internal class YieldBalancesConverter( val isActionable = rewards?.any { it.pendingActions.isNotEmpty() } == true val isRewardsClaimable = rewards?.isNotEmpty() == true - val isSolana = isSolana(cryptoCurrencyStatus.currency.network.id.value) - return when { - isSolana -> RewardBlockType.RewardUnavailable to false + isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable to false isRewardsClaimable -> RewardBlockType.Rewards to isActionable else -> RewardBlockType.NoRewards to false } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt index 55446f2303..c84db7f3ea 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt @@ -1,6 +1,9 @@ package com.tangem.features.staking.impl.presentation.state.helpers import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase +import com.tangem.domain.staking.FetchActionsUseCase +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus import com.tangem.domain.tokens.FetchPendingTransactionsUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -20,16 +23,21 @@ internal class StakingBalanceUpdater @AssistedInject constructor( private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase, private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val getTxHistoryItemsUseCase: GetTxHistoryItemsUseCase, + private val fetchActionsUseCase: FetchActionsUseCase, @DelayedWork private val coroutineScope: CoroutineScope, @Assisted private val userWallet: UserWallet, @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, + @Assisted private val yield: Yield, ) { - fun scheduleUpdates() { + fun fullUpdate() { coroutineScope.launch { listOf( // we should update network to find pending tx after 1 sec async { - fetchPendingTransactionsUseCase(userWallet.walletId, setOf(cryptoCurrencyStatus.currency.network)) + fetchPendingTransactionsUseCase( + userWalletId = userWallet.walletId, + networks = setOf(cryptoCurrencyStatus.currency.network), + ) }, // we should update tx history and network for new balances async { @@ -41,18 +49,34 @@ internal class StakingBalanceUpdater @AssistedInject constructor( async { updateNetworkStatuses() }, + async { + updateProcessingActions() + }, ).awaitAll() } } - suspend fun instantUpdate() { + suspend fun partialUpdate() { + coroutineScope { + listOf( + async { + updateNetworkStatuses(delay = 0) + }, + async { + updateProcessingActions() + }, + ).awaitAll() + } + } + + suspend fun initialUpdate() { coroutineScope { listOf( async { updateStakeBalance() }, async { - updateNetworkStatuses(delay = 0) + updateProcessingActions() }, ).awaitAll() } @@ -91,9 +115,22 @@ internal class StakingBalanceUpdater @AssistedInject constructor( } } + private suspend fun updateProcessingActions() { + fetchActionsUseCase( + userWalletId = userWallet.walletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + networkType = yield.token.network, + stakingActionStatus = StakingActionStatus.PROCESSING, + ) + } + @AssistedFactory interface Factory { - fun create(cryptoCurrencyStatus: CryptoCurrencyStatus, userWallet: UserWallet): StakingBalanceUpdater + fun create( + cryptoCurrencyStatus: CryptoCurrencyStatus, + userWallet: UserWallet, + yield: Yield, + ): StakingBalanceUpdater } private companion object { diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt index a49da3f88d..9167845687 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingFeeTransactionLoader.kt @@ -8,7 +8,6 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.extensions.isZero import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.domain.staking.EstimateGasUseCase -import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.Yield @@ -17,13 +16,11 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.domain.transaction.usecase.GetAllowanceUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.presentation.state.StakingStateController import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.ValidatorState -import com.tangem.features.staking.impl.presentation.state.utils.isSolanaWithdraw +import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions import com.tangem.utils.extensions.orZero import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -38,18 +35,14 @@ import java.math.BigDecimal @Suppress("LongParameterList") internal class StakingFeeTransactionLoader @AssistedInject constructor( private val stateController: StakingStateController, - private val getAllowanceUseCase: GetAllowanceUseCase, private val getFeeUseCase: GetFeeUseCase, private val estimateGasUseCase: EstimateGasUseCase, @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, @Assisted private val userWallet: UserWallet, @Assisted private val yield: Yield, - @Assisted private val stakingApproval: StakingApproval, ) { suspend fun getFee( - pendingAction: PendingAction?, - pendingActions: ImmutableList?, onStakingFee: (Fee) -> Unit, onStakingFeeError: (StakingError) -> Unit, onApprovalFee: (TransactionFee) -> Unit, @@ -57,40 +50,28 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor( ) { val state = stateController.value val confirmationState = state.confirmationState as? StakingStates.ConfirmationState.Data - ?: error("No confirmation state") - val validatorState = confirmationState.validatorState as? ValidatorState.Content + ?: error("Illegal state") + val validatorState = state.validatorState as? StakingStates.ValidatorState.Data ?: error("No validator provided") val amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: error("No amount provided") + val pendingAction = confirmationState.pendingAction + val pendingActions = confirmationState.pendingActions + val validatorAddress = validatorState.chosenValidator.address - val approval = stakingApproval as? StakingApproval.Needed - if (approval != null && state.actionType == StakingActionCommonType.ENTER) { - val allowance = getAllowanceUseCase( - userWalletId = userWallet.walletId, - cryptoCurrency = cryptoCurrencyStatus.currency, - spenderAddress = approval.spenderAddress, - ).getOrElse { BigDecimal.ZERO } - - if (allowance < amount) { - getApproveFee( - amount = amount, - validatorAddress = validatorAddress, - onApprovalFee = onApprovalFee, - onApprovalFeeError = onFeeError, - ) - } else { - estimateGas( - pendingAction = pendingAction, - pendingActions = pendingActions, - amount = amount, - validatorAddress = validatorAddress, - onStakingFeeError = onStakingFeeError, - onStakingFee = onStakingFee, - ) - } + val isEnter = state.actionType == StakingActionCommonType.Enter + val isApprovalNeeded = confirmationState.isApprovalNeeded + val isAllowanceNotEnough = confirmationState.allowance < amount + if (isEnter && isApprovalNeeded && isAllowanceNotEnough) { + getApproveFee( + amount = amount, + validatorAddress = validatorAddress, + onApprovalFee = onApprovalFee, + onApprovalFeeError = onFeeError, + ) } else { estimateGas( pendingAction = pendingAction, @@ -114,7 +95,11 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor( val sourceAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: error("No available address") - val gasEstimate = if (isSolanaWithdraw(cryptoCurrencyStatus.currency.network.id.value, pendingActions)) { + val gasEstimate = if (isCompositePendingActions( + networkId = cryptoCurrencyStatus.currency.network.id.value, + pendingActions = pendingActions, + ) + ) { val result = coroutineScope { pendingActions?.map { action -> async { @@ -234,7 +219,6 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor( cryptoCurrencyStatus: CryptoCurrencyStatus, userWallet: UserWallet, yield: Yield, - stakingApproval: StakingApproval, ): StakingFeeTransactionLoader } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt index addda5d313..3ccad868cc 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingTransactionSender.kt @@ -5,7 +5,6 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.domain.staking.* -import com.tangem.domain.staking.model.PendingTransaction import com.tangem.domain.staking.model.SubmitHashData import com.tangem.domain.staking.model.stakekit.* import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType @@ -21,7 +20,7 @@ import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount -import com.tangem.features.staking.impl.presentation.state.utils.isSolanaWithdraw +import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions import com.tangem.utils.extensions.orZero import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -31,7 +30,6 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import timber.log.Timber import java.math.BigDecimal -import java.util.UUID @Suppress("LongParameterList") internal class StakingTransactionSender @AssistedInject constructor( @@ -43,7 +41,6 @@ internal class StakingTransactionSender @AssistedInject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val submitHashUseCase: SubmitHashUseCase, private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase, - private val savePendingTransactionUseCase: SavePendingTransactionUseCase, @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, @Assisted private val userWallet: UserWallet, @Assisted private val yield: Yield, @@ -51,7 +48,7 @@ internal class StakingTransactionSender @AssistedInject constructor( ) { private val balanceUpdater: StakingBalanceUpdater - get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet) + get() = stakingBalanceUpdater.create(cryptoCurrencyStatus, userWallet, yield) suspend fun constructAndSendTransactions( onConstructSuccess: (List) -> Unit, @@ -89,26 +86,21 @@ internal class StakingTransactionSender @AssistedInject constructor( sendStakingTransaction( fullTransactionsData = fullTransactionsData, - possiblePendingTransaction = composePendingTransaction(confirmationState), onSendSuccess = onSendSuccess, onSendError = onSendError, ) } - private fun composePendingTransaction(confirmationState: StakingStates.ConfirmationState.Data): PendingTransaction { - return confirmationState.possiblePendingTransaction ?: composeStakeTransaction(confirmationState) - } - private suspend fun getStakingTransactions( state: StakingUiState, confirmationState: StakingStates.ConfirmationState.Data, onConstructError: (StakingError) -> Unit, ) = coroutineScope { - val isAllWithdrawAction = isSolanaWithdraw( + val isComposePendingActions = isCompositePendingActions( cryptoCurrencyStatus.currency.network.id.value, confirmationState.pendingActions, ) - if (isAllWithdrawAction) { + if (isComposePendingActions) { confirmationState.pendingActions?.map { action -> async { getStakingTransaction( @@ -169,7 +161,7 @@ internal class StakingTransactionSender @AssistedInject constructor( action: PendingAction? = confirmationState.pendingAction, onConstructError: (StakingError) -> Unit, ): List { - val validatorState = confirmationState.validatorState as? ValidatorState.Content + val validatorState = state.validatorState as? StakingStates.ValidatorState.Data ?: error("No validator provided") val fee = (confirmationState.feeState as? FeeState.Content)?.fee ?: error("No fee provided") @@ -202,7 +194,6 @@ internal class StakingTransactionSender @AssistedInject constructor( private suspend fun sendStakingTransaction( fullTransactionsData: List, - possiblePendingTransaction: PendingTransaction, onSendSuccess: (txUrl: String) -> Unit, onSendError: (SendTransactionError?) -> Unit, ) { @@ -218,24 +209,19 @@ internal class StakingTransactionSender @AssistedInject constructor( submitHash( transactions = fullTransactionsData.map { it.stakeKitTransaction }, transactionHashes = transactionHashes, - pendingTransaction = possiblePendingTransaction, ) val txUrl = getExplorerTransactionUrlUseCase( txHash = transactionHashes.last(), networkId = cryptoCurrencyStatus.currency.network.id, ).getOrElse { "" } - balanceUpdater.scheduleUpdates() + balanceUpdater.fullUpdate() onSendSuccess(txUrl) }, ) } - private suspend fun submitHash( - transactions: List, - transactionHashes: List, - pendingTransaction: PendingTransaction, - ) { + private suspend fun submitHash(transactions: List, transactionHashes: List) { transactions .zip(transactionHashes) .forEach { (transaction, transactionHash) -> @@ -243,7 +229,6 @@ internal class StakingTransactionSender @AssistedInject constructor( SubmitHashData( transactionId = transaction.id, transactionHash = transactionHash, - pendingTransaction = pendingTransaction, ), ) .onLeft { @@ -253,9 +238,6 @@ internal class StakingTransactionSender @AssistedInject constructor( ) }.onRight { Timber.d("Successful hash submission") - if (transaction.type != StakingTransactionType.FREEZE_ENERGY) { - savePendingTransactionUseCase.invoke(userWallet.walletId, pendingTransaction) - } } } } @@ -263,7 +245,7 @@ internal class StakingTransactionSender @AssistedInject constructor( private fun getAmount(amountState: AmountState.Data, fee: Fee, reduceAmountBy: BigDecimal?): BigDecimal { val amountValue = amountState.amountTextField.cryptoAmount.value ?: error("No amount value") val feeValue = fee.amount.value ?: error("No fee value") - val isEnterAction = stateController.value.actionType == StakingActionCommonType.ENTER + val isEnterAction = stateController.value.actionType == StakingActionCommonType.Enter return checkAndCalculateSubtractedAmount( isAmountSubtractAvailable = isAmountSubtractAvailable && isEnterAction, @@ -274,23 +256,6 @@ internal class StakingTransactionSender @AssistedInject constructor( ) } - private fun composeStakeTransaction(confirmationState: StakingStates.ConfirmationState.Data): PendingTransaction { - val state = stateController.value - - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data - val token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId) - - return PendingTransaction( - groupId = UUID.randomUUID().toString(), - token = token, - type = BalanceType.STAKED, - amount = (state.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: BigDecimal.ZERO, - rawCurrencyId = cryptoCurrencyStatus.currency.id.rawCurrencyId, - validator = (confirmationState.validatorState as? ValidatorState.Content)?.chosenValidator, - balancesId = yieldBalance?.getBalancesUniqueId() ?: 0, - ) - } - private data class FullTransactionData( val stakeKitTransaction: StakingTransaction, val tangemTransaction: TransactionData.Compiled, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index d9c7263d8e..ac86e7084a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -7,8 +7,6 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.* import kotlinx.collections.immutable.persistentListOf @@ -16,48 +14,6 @@ import java.math.BigDecimal internal object ConfirmationStatePreviewData { - private val validatorList = listOf( - Yield.Validator( - address = "0xa6e768fef2d1af36c0cfdb276422e7881a83e951", - status = ValidatorStatus.ACTIVE, - name = "Luganodes", - image = "https://assets.stakek.it/validators/luganodes.png", - apr = BigDecimal("0.054823398040640445"), - commission = 0.1, - stakedBalance = "355544384.45009977", - website = "https://luganodes.com/", - votingPower = 0.09778360195377911, - preferred = true, - isStrategicPartner = false, - ), - Yield.Validator( - address = "0x35b1ca0f398905cf752e6fe122b51c88022fca32", - status = ValidatorStatus.ACTIVE, - name = "InfStones", - image = "https://assets.stakek.it/validators/infstones.png", - apr = BigDecimal("0.057786472172836965"), - commission = 0.05, - stakedBalance = "12495684.05643019", - website = "https://infstones.com/", - votingPower = 0.0034366257754399774, - preferred = true, - isStrategicPartner = false, - ), - Yield.Validator( - address = "0xd14a87025109013b0a2354a775cb335f926af65a", - status = ValidatorStatus.ACTIVE, - name = "Kiln", - image = "https://assets.stakek.it/validators/kiln.png", - apr = BigDecimal("0.057786472172836965"), - commission = 0.05, - stakedBalance = "85400369.96393165", - website = "https://infstones.com/", - votingPower = 0.023487238579718264, - preferred = true, - isStrategicPartner = true, - ), - ) - private val fee = Fee.Common( amount = Amount( currencySymbol = "MATIC", @@ -77,11 +33,6 @@ internal object ConfirmationStatePreviewData { isFeeApproximate = false, isFeeConvertibleToFiat = true, ), - validatorState = ValidatorState.Content( - isClickable = true, - chosenValidator = validatorList[0], - availableValidators = validatorList, - ), footerText = stringReference("You stake \$715.11 and will be receiving ~\$35 monthly"), notifications = persistentListOf( StakingNotification.Info.EarnRewards( @@ -94,8 +45,8 @@ internal object ConfirmationStatePreviewData { transactionDoneState = TransactionDoneState.Empty, pendingAction = null, isApprovalNeeded = false, + allowance = BigDecimal.ZERO, reduceAmountBy = null, pendingActions = null, - possiblePendingTransaction = null, ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index 4d3dc0ad83..9bc454ab92 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -68,14 +68,15 @@ internal object InitialStakingStatePreview { rewardsCrypto = "100 SOL", rewardBlockType = RewardBlockType.RewardUnavailable, isActionable = true, - balance = persistentListOf( + balances = persistentListOf( BalanceState( groupId = "groupId", title = stringReference("Binance"), cryptoValue = "100", - cryptoAmount = stringReference("100 SOL"), - cryptoDecimal = "100".toBigDecimal(), - fiatAmount = stringReference("100 $"), + formattedCryptoAmount = stringReference("100 SOL"), + cryptoAmount = "100".toBigDecimal(), + fiatAmount = null, + formattedFiatAmount = stringReference("100 $"), rawCurrencyId = null, validator = Yield.Validator( address = "address", diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ValidatorStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ValidatorStatePreviewData.kt new file mode 100644 index 0000000000..225057c19c --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ValidatorStatePreviewData.kt @@ -0,0 +1,60 @@ +package com.tangem.features.staking.impl.presentation.state.previewdata + +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus +import com.tangem.features.staking.impl.presentation.state.StakingStates +import java.math.BigDecimal + +internal object ValidatorStatePreviewData { + + private val validatorList = listOf( + Yield.Validator( + address = "0xa6e768fef2d1af36c0cfdb276422e7881a83e951", + status = ValidatorStatus.ACTIVE, + name = "Luganodes", + image = "https://assets.stakek.it/validators/luganodes.png", + apr = BigDecimal("0.054823398040640445"), + commission = 0.1, + stakedBalance = "355544384.45009977", + website = "https://luganodes.com/", + votingPower = 0.09778360195377911, + preferred = true, + isStrategicPartner = false, + ), + Yield.Validator( + address = "0x35b1ca0f398905cf752e6fe122b51c88022fca32", + status = ValidatorStatus.ACTIVE, + name = "InfStones", + image = "https://assets.stakek.it/validators/infstones.png", + apr = BigDecimal("0.057786472172836965"), + commission = 0.05, + stakedBalance = "12495684.05643019", + website = "https://infstones.com/", + votingPower = 0.0034366257754399774, + preferred = true, + isStrategicPartner = true, + ), + Yield.Validator( + address = "0xd14a87025109013b0a2354a775cb335f926af65a", + status = ValidatorStatus.ACTIVE, + name = "Kiln", + image = "https://assets.stakek.it/validators/kiln.png", + apr = BigDecimal("0.057786472172836965"), + commission = 0.05, + stakedBalance = "85400369.96393165", + website = "https://infstones.com/", + votingPower = 0.023487238579718264, + preferred = true, + isStrategicPartner = false, + ), + ) + + val validatorState = StakingStates.ValidatorState.Data( + availableValidators = validatorList, + chosenValidator = validatorList.first(), + isPrimaryButtonEnabled = true, + activeValidator = null, + isClickable = true, + isVisibleOnConfirmation = true, + ) +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 716cab0e93..514a94fc76 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -2,14 +2,11 @@ package com.tangem.features.staking.impl.presentation.state.stub import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents -import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @Suppress("TooManyFunctions") @@ -17,13 +14,7 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onBackClick() {} - override fun onNextClick( - actionTypeToOverwrite: StakingActionCommonType?, - pendingAction: PendingAction?, - pendingActions: ImmutableList?, - balanceState: BalanceState?, - ) { - } + override fun onNextClick(balanceState: BalanceState?) {} override fun onActionClick() {} @@ -35,7 +26,7 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onInfoClick(infoType: InfoType) {} - override fun onEnterClick() {} + override fun onAmountEnterClick() {} override fun onAmountValueChange(value: String) {} @@ -67,8 +58,7 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onActiveStake(activeStake: BalanceState) {} - override fun getFee(pendingAction: PendingAction?, pendingActions: ImmutableList?) { - } + override fun getFee() {} override fun onAmountReduceByClick( reduceAmountBy: BigDecimal, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ActionTypeActiveStakeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ActionTypeActiveStakeTransformer.kt deleted file mode 100644 index 31e1f97f96..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/ActionTypeActiveStakeTransformer.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.features.staking.impl.presentation.state.transformers - -import com.tangem.domain.staking.model.stakekit.BalanceType -import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Network -import com.tangem.features.staking.impl.presentation.state.* -import com.tangem.lib.crypto.BlockchainUtils.isTron -import com.tangem.utils.transformer.Transformer - -internal class ActionTypeActiveStakeTransformer( - private val cryptoCurrencyStatus: CryptoCurrencyStatus, - private val activeStake: BalanceState, -) : Transformer { - - override fun transform(prevState: StakingUiState): StakingUiState { - val isTronStakedBalance = isTronStakedBalance( - networkId = cryptoCurrencyStatus.currency.network.id, - activeStake = activeStake, - ) - val actionType = if (activeStake.pendingActions.isEmpty() || isTronStakedBalance) { - StakingActionCommonType.EXIT - } else { - StakingActionCommonType.PENDING_OTHER - } - - return prevState.copy(actionType = actionType) - } - - private fun isTronStakedBalance(networkId: Network.ID, activeStake: BalanceState): Boolean { - return isTron(networkId.value) && activeStake.type == BalanceType.STAKED - } -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetActionToExecuteTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetActionToExecuteTransformer.kt deleted file mode 100644 index e3adab9f50..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetActionToExecuteTransformer.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.features.staking.impl.presentation.state.transformers - -import com.tangem.domain.staking.model.stakekit.PendingAction -import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.ImmutableList - -internal class SetActionToExecuteTransformer( - private val actionTypeToOverwrite: StakingActionCommonType, - private val pendingAction: PendingAction?, - private val pendingActions: ImmutableList?, -) : Transformer { - override fun transform(prevState: StakingUiState): StakingUiState { - val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data - return prevState.copy( - actionType = actionTypeToOverwrite, - confirmationState = confirmationState?.copy( - pendingAction = pendingAction, - pendingActions = pendingActions, - ) ?: StakingStates.ConfirmationState.Empty(), - ) - } -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt new file mode 100644 index 0000000000..532054c768 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt @@ -0,0 +1,61 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.common.ui.amountScreen.converters.AmountStateConverter +import com.tangem.common.ui.amountScreen.models.AmountParameters +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import com.tangem.utils.Provider +import com.tangem.utils.transformer.Transformer + +internal class SetAmountDataTransformer( + private val clickIntents: StakingClickIntents, + private val cryptoCurrencyStatusProvider: Provider, + private val userWalletProvider: Provider, + private val appCurrencyProvider: Provider, +) : Transformer { + + private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) + + override fun transform(prevState: StakingUiState): StakingUiState { + val title = if (prevState.actionType == StakingActionCommonType.Exit) { + resourceReference(R.string.staking_staked_amount) + } else { + stringReference(userWalletProvider().name) + } + val cryptoBalanceValue = cryptoCurrencyStatusProvider().value + val (amount, fiatAmount) = if (prevState.actionType != StakingActionCommonType.Enter) { + prevState.balanceState?.cryptoAmount to prevState.balanceState?.fiatAmount + } else { + cryptoBalanceValue.amount to cryptoBalanceValue.fiatAmount + } + val maxEnterAmount = EnterAmountBoundary( + amount = amount, + fiatAmount = fiatAmount, + fiatRate = cryptoBalanceValue.fiatRate, + ) + + return prevState.copy( + amountState = AmountStateConverter( + clickIntents = clickIntents, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + appCurrencyProvider = appCurrencyProvider, + iconStateConverter = iconStateConverter, + maxEnterAmount = maxEnterAmount, + ).convert( + AmountParameters( + title = title, + value = "", + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index 52251d7408..e989a47670 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.core.navigation.url.UrlOpener @@ -9,6 +10,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.utils.getPendingActionTitle +import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -109,28 +111,30 @@ internal class SetButtonsStateTransformer( StakingStep.Confirmation -> getConfirmationButtonText() StakingStep.Validators -> resourceReference(R.string.common_continue) StakingStep.Amount, + StakingStep.RestakeValidator, StakingStep.RewardsValidators, -> resourceReference(R.string.common_next) } } private fun StakingUiState.getConfirmationButtonText(): TextReference { - return if (confirmationState is StakingStates.ConfirmationState.Data) { + val confirmationState = confirmationState as? StakingStates.ConfirmationState.Data + val amountState = amountState as? AmountState.Data + return if (confirmationState != null && amountState != null) { if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { resourceReference(R.string.common_close) } else { when (actionType) { - StakingActionCommonType.ENTER -> { - if (confirmationState.isApprovalNeeded) { + StakingActionCommonType.Enter -> { + val amount = amountState.amountTextField.cryptoAmount.value.orZero() + if (confirmationState.isApprovalNeeded && confirmationState.allowance < amount) { resourceReference(R.string.give_permission_title) } else { resourceReference(R.string.common_stake) } } - StakingActionCommonType.EXIT -> resourceReference(R.string.common_unstake) - StakingActionCommonType.PENDING_OTHER, - StakingActionCommonType.PENDING_REWARDS, - -> confirmationState.pendingAction?.type.getPendingActionTitle() + StakingActionCommonType.Exit -> resourceReference(R.string.common_unstake) + is StakingActionCommonType.Pending -> confirmationState.pendingAction?.type.getPendingActionTitle() } } } else { @@ -140,32 +144,26 @@ internal class SetButtonsStateTransformer( private fun StakingUiState.onPrimaryClick() { when (currentStep) { - StakingStep.InitialInfo -> clickIntents.onEnterClick() - StakingStep.Validators -> { - if (confirmationState is StakingStates.ConfirmationState.Data) { - clickIntents.onNextClick( - pendingActions = confirmationState.pendingActions, - pendingAction = confirmationState.pendingAction, - ) - } else { - clickIntents.onNextClick() - } - } - StakingStep.Amount -> clickIntents.onNextClick() + StakingStep.InitialInfo -> clickIntents.onNextClick() + StakingStep.Validators, + StakingStep.RestakeValidator, + -> clickIntents.onNextClick() + StakingStep.Amount -> clickIntents.onAmountEnterClick() StakingStep.Confirmation -> onConfirmationClick() StakingStep.RewardsValidators -> Unit } } private fun StakingUiState.onConfirmationClick() { - if (confirmationState is StakingStates.ConfirmationState.Data) { + val confirmationState = confirmationState as? StakingStates.ConfirmationState.Data + val amountState = amountState as? AmountState.Data + if (confirmationState != null && amountState != null) { if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { clickIntents.onNextClick() } else { - val isEnterAction = actionType == StakingActionCommonType.ENTER - val isApproveNeeded = confirmationState.isApprovalNeeded - - if (isEnterAction && isApproveNeeded) { + val amount = amountState.amountTextField.cryptoAmount.value.orZero() + val isEnterAction = actionType == StakingActionCommonType.Enter + if (isEnterAction && confirmationState.isApprovalNeeded && confirmationState.allowance < amount) { clickIntents.showApprovalBottomSheet() } else { clickIntents.onActionClick() @@ -179,6 +177,7 @@ internal class SetButtonsStateTransformer( private fun StakingStep.isPrevButtonVisible(): Boolean = when (this) { StakingStep.InitialInfo, StakingStep.RewardsValidators, + StakingStep.RestakeValidator, StakingStep.Confirmation, StakingStep.Validators, -> false @@ -192,7 +191,9 @@ internal class SetButtonsStateTransformer( StakingStep.Amount -> amountState.isPrimaryButtonEnabled StakingStep.Confirmation -> confirmationState.isPrimaryButtonEnabled StakingStep.RewardsValidators -> rewardsValidatorsState.isPrimaryButtonEnabled - StakingStep.Validators -> true + StakingStep.RestakeValidator, + StakingStep.Validators, + -> true } } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt index 196784785f..85443b4096 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateAssentTransformer.kt @@ -2,22 +2,17 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.FeeState -import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.ImmutableList internal class SetConfirmationStateAssentTransformer( private val appCurrencyProvider: Provider, private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, private val fee: Fee, - private val action: PendingAction?, - private val actions: ImmutableList?, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { @@ -30,7 +25,6 @@ internal class SetConfirmationStateAssentTransformer( if (this is StakingStates.ConfirmationState.Data) { val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true return copy( - innerState = InnerConfirmationStakingState.ASSENT, feeState = FeeState.Content( fee = fee, rate = feeCryptoCurrencyStatus?.value?.fiatRate, @@ -38,11 +32,7 @@ internal class SetConfirmationStateAssentTransformer( appCurrency = appCurrencyProvider(), isFeeApproximate = false, ), - validatorState = validatorState.copySealed(isClickable = true), - pendingAction = action, - pendingActions = actions, isPrimaryButtonEnabled = true, - isApprovalNeeded = false, ) } else { return this diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index db27e0c2d9..4ecbf95344 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -16,11 +16,10 @@ internal class SetConfirmationStateCompletedTransformer( } private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState { - if (this is StakingStates.ConfirmationState.Data) { - return copy( + return if (this is StakingStates.ConfirmationState.Data) { + copy( isPrimaryButtonEnabled = true, innerState = InnerConfirmationStakingState.COMPLETED, - validatorState = validatorState.copySealed(isClickable = false), footerText = TextReference.EMPTY, notifications = persistentListOf(), transactionDoneState = TransactionDoneState.Content( @@ -29,7 +28,7 @@ internal class SetConfirmationStateCompletedTransformer( ), ) } else { - return this + this } } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateEmptyTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateEmptyTransformer.kt new file mode 100644 index 0000000000..4bf8d5ce5d --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateEmptyTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal object SetConfirmationStateEmptyTransformer : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + actionType = StakingActionCommonType.Enter, + validatorState = StakingStates.ValidatorState.Empty(), + confirmationState = StakingStates.ConfirmationState.Empty(), + balanceState = null, + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt index 24bdd43c84..9043cc8ae9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateInProgressTransformer.kt @@ -19,7 +19,6 @@ internal class SetConfirmationStateInProgressTransformer : Transformer? = null, + private val pendingAction: PendingAction? = pendingActions?.firstOrNull(), + +) : Transformer { + + private val networkId + get() = cryptoCurrencyStatus.currency.network.id.value + + private val isComposePendingActions + get() = isCompositePendingActions(networkId, pendingActions) + + private val isTronStakedBalance + get() = isTronStakedBalance(networkId, pendingAction) + + private val isImplicitExit: Boolean + get() = pendingAction == null && pendingActions?.isEmpty() == true || isTronStakedBalance + + override fun transform(prevState: StakingUiState): StakingUiState { + val actionType = when { + isEnter -> StakingActionCommonType.Enter + isImplicitExit || isExplicitExit -> StakingActionCommonType.Exit + else -> when (pendingAction?.type) { + StakingActionType.STAKE -> StakingActionCommonType.Enter + StakingActionType.UNSTAKE -> StakingActionCommonType.Exit + StakingActionType.CLAIM_REWARDS, + StakingActionType.RESTAKE_REWARDS, + -> StakingActionCommonType.Pending.Rewards + StakingActionType.VOTE_LOCKED, + StakingActionType.RESTAKE, + -> StakingActionCommonType.Pending.Restake + else -> StakingActionCommonType.Pending.Other + } + } + + return prevState.copy( + actionType = actionType, + balanceState = balanceState, + confirmationState = StakingStates.ConfirmationState.Data( + isPrimaryButtonEnabled = false, + innerState = InnerConfirmationStakingState.ASSENT, + feeState = FeeState.Loading, + notifications = persistentListOf(), + footerText = TextReference.EMPTY, + transactionDoneState = TransactionDoneState.Empty, + isApprovalNeeded = stakingApproval is StakingApproval.Needed, + allowance = stakingAllowance, + reduceAmountBy = null, + pendingAction = pendingAction, + pendingActions = pendingActions.takeIf { isComposePendingActions }, + ), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index 01e54c47d2..723a0ffd7c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -13,7 +13,6 @@ import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.utils.getRewardScheduleText import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.persistentListOf internal class SetConfirmationStateLoadingTransformer( private val yield: Yield, @@ -22,39 +21,21 @@ internal class SetConfirmationStateLoadingTransformer( ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { - val filteredValidators = yield.validators.filter { - it.preferred - } val possibleConfirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data - val possibleValidatorState = possibleConfirmationState?.validatorState as? ValidatorState.Content - val chosenValidator = possibleValidatorState?.chosenValidator ?: filteredValidators[0] return prevState.copy( - confirmationState = StakingStates.ConfirmationState.Data( + confirmationState = possibleConfirmationState?.copy( isPrimaryButtonEnabled = false, - innerState = InnerConfirmationStakingState.ASSENT, feeState = FeeState.Loading, - validatorState = ValidatorState.Content( - isClickable = true, - chosenValidator = chosenValidator, - availableValidators = filteredValidators, - ), - notifications = persistentListOf(), footerText = getFooter(prevState), - transactionDoneState = TransactionDoneState.Empty, - pendingAction = possibleConfirmationState?.pendingAction, - pendingActions = possibleConfirmationState?.pendingActions, - isApprovalNeeded = false, - reduceAmountBy = null, - possiblePendingTransaction = null, - ), + ) ?: prevState.confirmationState, ) } private fun getFooter(state: StakingUiState): TextReference { val amountState = state.amountState as? AmountState.Data - val isEnterAction = state.actionType == StakingActionCommonType.ENTER + val isEnterAction = state.actionType == StakingActionCommonType.Enter val amountDecimal = amountState?.amountTextField?.fiatAmount?.value val amountValue = BigDecimalFormatter.formatFiatAmount( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt index ee866db72c..1c1c7103dc 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateResetAssentTransformer.kt @@ -13,7 +13,6 @@ internal object SetConfirmationStateResetAssentTransformer : Transformer, private val userWalletProvider: Provider, @@ -43,16 +45,6 @@ internal class SetInitialDataStateTransformer( private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) - private val amountStateConverter by lazy(LazyThreadSafetyMode.NONE) { - AmountStateConverter( - clickIntents = clickIntents, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, - appCurrencyProvider = appCurrencyProvider, - userWalletProvider = userWalletProvider, - iconStateConverter = iconStateConverter, - ) - } - private val rewardsValidatorStateConverter by lazy(LazyThreadSafetyMode.NONE) { RewardsValidatorStateConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield) } @@ -72,11 +64,12 @@ internal class SetInitialDataStateTransformer( title = TextReference.EMPTY, cryptoCurrencyName = cryptoCurrency.name, cryptoCurrencySymbol = cryptoCurrency.symbol, + cryptoCurrencyBlockchainId = cryptoCurrency.network.id.value, clickIntents = clickIntents, currentStep = StakingStep.InitialInfo, initialInfoState = createInitialInfoState(), amountState = createInitialAmountState(), - confirmationState = createInitialConfirmationState(), + confirmationState = StakingStates.ConfirmationState.Empty(), rewardsValidatorsState = rewardsValidatorStateConverter.convert(Unit), bottomSheetConfig = null, ) @@ -87,7 +80,7 @@ internal class SetInitialDataStateTransformer( return StakingStates.InitialInfoState.Data( isPrimaryButtonEnabled = !cryptoCurrencyStatusProvider().value.amount.isNullOrZero(), showBanner = !isAnyTokenStaked && yieldBalance == InnerYieldBalanceState.Empty, - aprRange = getAprRange(yield.validators), + aprRange = getAprRange(yield.preferredValidators), infoItems = getInfoItems(), onInfoClick = clickIntents::onInfoClick, yieldBalance = yieldBalance, @@ -113,7 +106,7 @@ internal class SetInitialDataStateTransformer( } private fun createAnnualPercentageRateItem(): RoundedListWithDividersItemData { - val validators = yield.validators + val validators = yield.preferredValidators return RoundedListWithDividersItemData( id = R.string.staking_details_annual_percentage_rate, startText = TextReference.Res(R.string.staking_details_annual_percentage_rate), @@ -128,11 +121,7 @@ internal class SetInitialDataStateTransformer( id = R.string.staking_details_available, startText = TextReference.Res(R.string.staking_details_available), endText = TextReference.Str( - value = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = cryptoCurrencyStatus.value.amount, - cryptoCurrency = cryptoCurrencyStatus.currency.symbol, - decimals = cryptoCurrencyStatus.currency.decimals, - ), + value = cryptoCurrencyStatus.value.amount.format { crypto(cryptoCurrencyStatus.currency) }, ), isEndTextHideable = true, ) @@ -158,11 +147,7 @@ internal class SetInitialDataStateTransformer( val minimumCryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum ?: return null if (!isPolkadot(cryptoCurrencyStatus.currency.network.id.value)) return null - val formattedAmount = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = minimumCryptoAmount, - cryptoCurrency = cryptoCurrencyStatus.currency.symbol, - decimals = cryptoCurrencyStatus.currency.decimals, - ) + val formattedAmount = minimumCryptoAmount.format { crypto(cryptoCurrencyStatus.currency) } return RoundedListWithDividersItemData( id = R.string.staking_details_minimum_requirement, @@ -215,23 +200,23 @@ internal class SetInitialDataStateTransformer( } private fun createInitialAmountState(): AmountState { - return amountStateConverter.convert("") - } - - private fun createInitialConfirmationState(): StakingStates.ConfirmationState { - return StakingStates.ConfirmationState.Data( - isPrimaryButtonEnabled = false, - innerState = InnerConfirmationStakingState.ASSENT, - feeState = FeeState.Loading, - validatorState = ValidatorState.Loading, - notifications = persistentListOf(), - footerText = TextReference.EMPTY, - transactionDoneState = TransactionDoneState.Empty, - pendingAction = null, - pendingActions = null, - isApprovalNeeded = isApprovalNeeded, - reduceAmountBy = null, - possiblePendingTransaction = null, + val cryptoBalanceValue = cryptoCurrencyStatusProvider().value + val maxEnterAmount = EnterAmountBoundary( + amount = cryptoBalanceValue.amount, + fiatAmount = cryptoBalanceValue.fiatAmount, + fiatRate = cryptoBalanceValue.fiatRate, + ) + return AmountStateConverter( + clickIntents = clickIntents, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + appCurrencyProvider = appCurrencyProvider, + iconStateConverter = iconStateConverter, + maxEnterAmount = maxEnterAmount, + ).convert( + AmountParameters( + title = stringReference(userWalletProvider().name), + value = "", + ), ) } @@ -243,14 +228,8 @@ internal class SetInitialDataStateTransformer( val minApr = aprValues.min() val maxApr = aprValues.max() - val formattedMinApr = BigDecimalFormatter.formatPercent( - percent = minApr, - useAbsoluteValue = true, - ).remove("%") - val formattedMaxApr = BigDecimalFormatter.formatPercent( - percent = maxApr, - useAbsoluteValue = true, - ) + val formattedMinApr = minApr.format { percent() }.remove("%") + val formattedMaxApr = maxApr.format { percent() } if (maxApr - minApr < EQUALITY_THRESHOLD) { return stringReference("$formattedMinApr%") diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetPossiblePendingTransactionTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetPossiblePendingTransactionTransformer.kt deleted file mode 100644 index 26508e49b5..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetPossiblePendingTransactionTransformer.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.features.staking.impl.presentation.state.transformers - -import com.tangem.domain.staking.model.PendingTransaction -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.features.staking.impl.presentation.state.BalanceState -import com.tangem.features.staking.impl.presentation.state.StakingStates -import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.utils.transformer.Transformer - -internal class SetPossiblePendingTransactionTransformer( - private val yield: Yield, - private val balanceState: BalanceState, - private val cryptoCurrencyStatus: CryptoCurrencyStatus, -) : Transformer { - - override fun transform(prevState: StakingUiState): StakingUiState { - val possibleConfirmationState = - prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState - val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data - - val balancesId = yieldBalance?.getBalancesUniqueId() ?: 0 - val token = yield.getCurrentToken(cryptoCurrencyStatus.currency.id.rawCurrencyId) - - return prevState.copy( - confirmationState = possibleConfirmationState.copy( - possiblePendingTransaction = PendingTransaction( - groupId = balanceState.groupId, - token = token, - type = balanceState.type, - amount = balanceState.cryptoDecimal, - rawCurrencyId = balanceState.rawCurrencyId, - validator = balanceState.validator, - balancesId = balancesId, - ), - ), - ) - } -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt index 578af70403..a29c1a8a85 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt @@ -20,7 +20,9 @@ internal object SetTitleTransformer : Transformer { val title = when (currentStep) { StakingStep.Amount -> resourceReference(R.string.send_amount_label) - StakingStep.Validators -> resourceReference(R.string.staking_validators) + StakingStep.RestakeValidator, + StakingStep.Validators, + -> resourceReference(R.string.staking_validators) StakingStep.RewardsValidators -> resourceReference(R.string.common_claim_rewards) StakingStep.InitialInfo -> resourceReference( R.string.staking_title_stake, @@ -29,11 +31,11 @@ internal object SetTitleTransformer : Transformer { StakingStep.Confirmation -> { when (actionType) { - StakingActionCommonType.ENTER -> resourceReference( + StakingActionCommonType.Enter -> resourceReference( R.string.staking_title_stake, wrappedList(prevState.cryptoCurrencyName), ) - StakingActionCommonType.EXIT -> resourceReference( + StakingActionCommonType.Exit -> resourceReference( R.string.staking_title_unstake, wrappedList(prevState.cryptoCurrencyName), ) @@ -51,7 +53,7 @@ internal object SetTitleTransformer : Transformer { } } - val subtitle = if (currentStep == StakingStep.Confirmation && actionType == StakingActionCommonType.ENTER) { + val subtitle = if (currentStep == StakingStep.Confirmation && actionType == StakingActionCommonType.Enter) { stringReference(prevState.walletName) } else { null diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt index f91d878456..41fdc707cf 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt @@ -1,21 +1,40 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount +import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer internal class AmountChangeStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val minimumTransactionAmount: EnterAmountBoundary?, private val value: String, private val yield: Yield, ) : Transformer { + private val maxEnterAmountConverter = MaxEnterAmountConverter() + override fun transform(prevState: StakingUiState): StakingUiState { + val actionType = prevState.actionType + val maxEnterAmount = if (actionType == StakingActionCommonType.Exit) { + EnterAmountBoundary( + amount = prevState.balanceState?.cryptoAmount, + fiatAmount = prevState.balanceState?.fiatAmount, + fiatRate = cryptoCurrencyStatus.value.fiatRate, + ) + } else { + maxEnterAmountConverter.convert(cryptoCurrencyStatus) + } + val updatedAmountState = AmountFieldChangeTransformer( - cryptoCurrencyStatus, - value, + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxEnterAmount = maxEnterAmount, + minimumTransactionAmount = minimumTransactionAmount, + value = value, ).transform(prevState.amountState) return prevState.copy( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt index 088528794d..db2865b4ec 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt @@ -1,18 +1,39 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount -import com.tangem.common.ui.amountScreen.converters.field.AmountFieldMaxAmountTransformer +import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter +import com.tangem.common.ui.amountScreen.converters.field.AmountFieldSetMaxAmountTransformer +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer internal class AmountMaxValueStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val minimumTransactionAmount: EnterAmountBoundary?, + private val actionType: StakingActionCommonType, private val yield: Yield, ) : Transformer { + private val maxEnterAmountConverter = MaxEnterAmountConverter() + override fun transform(prevState: StakingUiState): StakingUiState { - val updatedAmountState = AmountFieldMaxAmountTransformer(cryptoCurrencyStatus).transform(prevState.amountState) + val maxEnterAmount = if (actionType == StakingActionCommonType.Exit) { + EnterAmountBoundary( + amount = prevState.balanceState?.cryptoAmount, + fiatAmount = prevState.balanceState?.fiatAmount, + fiatRate = cryptoCurrencyStatus.value.fiatRate, + ) + } else { + maxEnterAmountConverter.convert(cryptoCurrencyStatus) + } + + val updatedAmountState = AmountFieldSetMaxAmountTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + maxAmount = maxEnterAmount, + minAmount = minimumTransactionAmount, + ).transform(prevState.amountState) return prevState.copy( amountState = AmountRequirementStateTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt index 91468a21bd..f5e92f66ec 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt @@ -2,18 +2,24 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer internal class AmountReduceByStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val minimumTransactionAmount: EnterAmountBoundary?, private val value: ReduceByData, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( - amountState = AmountReduceByTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + amountState = AmountReduceByTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minimumTransactionAmount, + value = value, + ).transform(prevState.amountState), ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt index b826aae59e..5f7b5e5e57 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount import com.tangem.common.ui.amountScreen.converters.AmountReduceToTransformer +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer @@ -8,11 +9,16 @@ import java.math.BigDecimal internal class AmountReduceToStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val minimumTransactionAmount: EnterAmountBoundary?, private val value: BigDecimal, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( - amountState = AmountReduceToTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + amountState = AmountReduceToTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minimumTransactionAmount, + value = value, + ).transform(prevState.amountState), ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt index d669d5a7e9..cb0b0e31e9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt @@ -1,13 +1,14 @@ package com.tangem.features.staking.impl.presentation.state.transformers.amount +import androidx.annotation.StringRes import androidx.compose.ui.text.input.ImeAction import com.tangem.common.extensions.isZero import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.staking.model.stakekit.AddressArgument import com.tangem.domain.staking.model.stakekit.Yield @@ -25,25 +26,18 @@ internal class AmountRequirementStateTransformer( private val actionType: StakingActionCommonType, ) : Transformer { override fun transform(prevState: AmountState): AmountState { - val amountRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT] - - return if (prevState is AmountState.Data && amountRequirements != null) { + return if (prevState is AmountState.Data) { updateWithError( prevState, actionType, - amountRequirements, ) } else { prevState } } - private fun updateWithError( - amountState: AmountState.Data, - actionType: StakingActionCommonType, - amountRequirements: AddressArgument, - ): AmountState { - val isRequirementError = isRequirementError(amountState, amountRequirements) + private fun updateWithError(amountState: AmountState.Data, actionType: StakingActionCommonType): AmountState { + val requirementError = getRequirementError(amountState) val isIntegerOnlyError = isIntegerOnlyError(amountState, actionType) val cryptoAmount = amountState.amountTextField.cryptoAmount @@ -52,62 +46,78 @@ internal class AmountRequirementStateTransformer( val errorText = when { amountState.amountTextField.isError -> amountState.amountTextField.error - isRequirementError -> resourceReference( - R.string.staking_amount_requirement_error, - wrappedList( - BigDecimalFormatter.formatCryptoAmount( - amountRequirements.minimum, - cryptoCurrencyStatus.currency.symbol, - cryptoCurrencyStatus.currency.decimals, - ), - ), - ) - isIntegerOnlyError -> resourceReference( - R.string.staking_amount_tron_integer_error, - wrappedList(value), - ) - else -> TextReference.EMPTY + requirementError != null -> requirementError + isIntegerOnlyError -> when (actionType) { + StakingActionCommonType.Enter -> resourceReference( + R.string.staking_amount_tron_integer_error, + wrappedList(value), + ) + StakingActionCommonType.Exit -> resourceReference( + R.string.staking_amount_tron_integer_error_unstaking, + wrappedList(value), + ) + else -> null + } + else -> null } - val isError = amountState.amountTextField.isError || isRequirementError - return if (!errorText.isNullOrEmpty()) { - amountState.copy( - isPrimaryButtonEnabled = !isError, - amountTextField = amountState.amountTextField.copy( - isError = isError, - isWarning = isIntegerOnlyError, - error = errorText, - keyboardOptions = amountState.amountTextField.keyboardOptions.copy( - imeAction = ImeAction.None, - ), + val isError = amountState.amountTextField.isError || requirementError != null + return amountState.copy( + isPrimaryButtonEnabled = !isError, + amountTextField = amountState.amountTextField.copy( + isError = isError, + isWarning = isIntegerOnlyError, + error = errorText ?: amountState.amountTextField.error, + keyboardOptions = amountState.amountTextField.keyboardOptions.copy( + imeAction = ImeAction.None, ), - ) - } else { - amountState - } + ), + ) } - private fun isRequirementError(prevState: AmountState.Data, amountRequirements: AddressArgument): Boolean { - val amountDecimal = prevState.amountTextField.cryptoAmount.value ?: return false + private fun getRequirementError(prevState: AmountState.Data): TextReference? { + val amountDecimal = prevState.amountTextField.cryptoAmount.value ?: return null val isAlreadyErrorState = prevState.amountTextField.isError - val isAmountRequired = amountRequirements.required val isAmountZero = amountDecimal.isZero() - val isExceedsRequirements = - amountRequirements.maximum?.compareTo(amountDecimal) == -1 || - amountRequirements.minimum?.compareTo(amountDecimal) == 1 - return !isAmountZero && isAmountRequired && isExceedsRequirements && !isAlreadyErrorState + if (isAlreadyErrorState || isAmountZero) return null + + return when (actionType) { + StakingActionCommonType.Enter -> { + val enterRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT] + enterRequirements?.getError(amountDecimal, R.string.staking_amount_requirement_error) + } + StakingActionCommonType.Exit -> { + val exitRequirements = yield.args.exit?.args?.get(Yield.Args.ArgType.AMOUNT) + exitRequirements?.getError(amountDecimal, R.string.staking_unstake_amount_requirement_error) + } + else -> null + } } private fun isIntegerOnlyError(amountState: AmountState.Data, actionType: StakingActionCommonType): Boolean { val cryptoAmountValue = amountState.amountTextField.cryptoAmount.value ?: return false - val isEnter = actionType == StakingActionCommonType.ENTER + val isEnterOrExit = actionType == StakingActionCommonType.Enter || actionType == StakingActionCommonType.Exit val isTron = isTron(cryptoCurrencyStatus.currency.network.id.value) val isIntegerOnly = cryptoAmountValue.isZero() || cryptoAmountValue.remainder(BigDecimal.ONE).isZero() - return isEnter && isTron && !isIntegerOnly + return isEnterOrExit && isTron && !isIntegerOnly + } + + private fun AddressArgument.getError(amount: BigDecimal, @StringRes errorTextRes: Int): TextReference? { + val isExceedsRequirements = maximum?.compareTo(amount) == -1 || + minimum?.compareTo(amount) == 1 + + return resourceReference( + errorTextRes, + wrappedList( + minimum.format { + crypto(cryptoCurrencyStatus.currency) + }, + ), + ).takeIf { required && isExceedsRequirements } } data class Data( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt index 939c6877dc..6f331795e4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetConfirmationStateAssentApprovalTransformer.kt @@ -35,7 +35,6 @@ internal class SetConfirmationStateAssentApprovalTransformer( appCurrency = appCurrencyProvider(), isFeeApproximate = false, ), - validatorState = validatorState.copySealed(isClickable = true), isPrimaryButtonEnabled = true, isApprovalNeeded = true, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt index ecfbc65c33..8350e74f74 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/ShowApprovalBottomSheetTransformer.kt @@ -5,6 +5,8 @@ import com.tangem.common.ui.bottomsheet.permission.state.* import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -12,7 +14,6 @@ import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.features.staking.impl.presentation.state.ValidatorState import com.tangem.utils.Provider import com.tangem.utils.transformer.Transformer @@ -28,17 +29,15 @@ internal class ShowApprovalBottomSheetTransformer( val amountState = prevState.amountState as? AmountState.Data ?: return prevState val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState - val validatorState = confirmationState.validatorState as? ValidatorState.Content ?: return prevState + val validatorState = prevState.validatorState as? StakingStates.ValidatorState.Data ?: return prevState val feeState = confirmationState.feeState as? FeeState.Content ?: return prevState val fee = feeState.fee ?: return prevState val walletAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value.orEmpty() val validatorAddress = validatorState.chosenValidator.address - val feeCryptoValue = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = fee.amount.value, - cryptoCurrency = fee.amount.currencySymbol, - decimals = fee.amount.decimals, - ) + val feeCryptoValue = fee.amount.value.format { + crypto(fee.amount.currencySymbol, fee.amount.decimals) + } val feeFiatValue = BigDecimalFormatter.formatFiatAmount( fiatAmount = feeCryptoCurrencyStatus?.value?.fiatRate?.multiply(fee.amount.value), fiatCurrencyCode = appCurrencyProvider().code, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/confirmation/SetUpdatedAllowanceTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/confirmation/SetUpdatedAllowanceTransformer.kt new file mode 100644 index 0000000000..5d8677de4f --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/confirmation/SetUpdatedAllowanceTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.confirmation + +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal + +internal class SetUpdatedAllowanceTransformer( + private val allowance: BigDecimal, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + return prevState.copy( + confirmationState = confirmationState?.copy(allowance = allowance) ?: prevState.confirmationState, + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt similarity index 67% rename from features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt rename to features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index c8066c1dc2..530e3cc24b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -1,4 +1,4 @@ -package com.tangem.features.staking.impl.presentation.state.transformers +package com.tangem.features.staking.impl.presentation.state.transformers.notifications import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.notifications.NotificationUM @@ -11,21 +11,14 @@ import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountE import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications import com.tangem.core.ui.extensions.networkIconResId -import com.tangem.core.ui.extensions.pluralReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.error.GetFeeError -import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -33,8 +26,6 @@ import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount import com.tangem.features.staking.impl.presentation.state.utils.checkFeeCoverage import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.lib.crypto.BlockchainUtils.isCosmos -import com.tangem.lib.crypto.BlockchainUtils.isTron import com.tangem.utils.Provider import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer @@ -53,6 +44,13 @@ internal class AddStakingNotificationsTransformer( private val isSubtractAvailable: Boolean, private val yield: Yield, ) : Transformer { + + private val stakingInfoNotificationsFactory = StakingInfoNotificationsFactory( + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + yield = yield, + isSubtractAvailable = isSubtractAvailable, + ) + override fun transform(prevState: StakingUiState): StakingUiState { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val balance = cryptoCurrencyStatus.value.amount.orZero() @@ -65,7 +63,7 @@ internal class AddStakingNotificationsTransformer( val feeValue = feeState?.fee?.amount?.value.orZero() val reduceAmountBy = confirmationState.reduceAmountBy.orZero() - val isEnterAction = prevState.actionType == StakingActionCommonType.ENTER + val isEnterAction = prevState.actionType == StakingActionCommonType.Enter val isFeeCoverage = checkFeeCoverage( amountValue = amountValue, feeValue = feeValue, @@ -93,12 +91,7 @@ internal class AddStakingNotificationsTransformer( prevState = prevState, feeError = feeError, sendingAmount = sendingAmount, - onReload = { - prevState.clickIntents.getFee( - confirmationState.pendingAction, - confirmationState.pendingActions, - ) - }, + onReload = prevState.clickIntents::getFee, feeValue = feeValue, ) // warnings @@ -110,7 +103,13 @@ internal class AddStakingNotificationsTransformer( isFeeCoverage = isFeeCoverage && isEnterAction && !sendingAmount.equals(minimumRequirement), ) - addInfoNotifications(prevState) + stakingInfoNotificationsFactory.addInfoNotifications( + notifications = this, + prevState = prevState, + sendingAmount = sendingAmount, + actionAmount = amountValue, + feeValue = feeValue, + ) }.toImmutableList() return prevState.copy( @@ -228,7 +227,7 @@ internal class AddStakingNotificationsTransformer( val showNotification = sendingAmount + feeAmount > balance if (showNotification) { - val notification = if (actionType == StakingActionCommonType.ENTER) { + val notification = if (actionType == StakingActionCommonType.Enter) { NotificationUM.Error.TotalExceedsBalance } else { with(cryptoCurrencyStatus.currency) { @@ -246,101 +245,4 @@ internal class AddStakingNotificationsTransformer( add(notification) } } - - private fun MutableList.addInfoNotifications(prevState: StakingUiState) { - when (prevState.actionType) { - StakingActionCommonType.EXIT -> addExitInfoNotifications() - StakingActionCommonType.ENTER -> addEnterInfoNotifications() - else -> addPendingInfoNotifications(prevState) - } - } - - private fun MutableList.addExitInfoNotifications() { - val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days - if (cooldownPeriodDays != null) { - add( - StakingNotification.Info.Unstake( - cooldownPeriodDays = cooldownPeriodDays, - subtitleRes = if (isCosmos(cryptoCurrencyStatusProvider().currency.network.id.value)) { - R.string.staking_notification_unstake_cosmos_text - } else { - R.string.staking_notification_unstake_text - }, - ), - ) - } - } - - private fun MutableList.addEnterInfoNotifications() { - addTronRevoteNotification() - } - - private fun MutableList.addPendingInfoNotifications(prevState: StakingUiState) { - val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data - val pendingActionType = confirmationState?.pendingAction?.type - val (titleReference, textReference) = when (pendingActionType) { - StakingActionType.CLAIM_REWARDS -> { - resourceReference(R.string.common_claim) to - resourceReference(R.string.staking_notification_claim_rewards_text) - } - StakingActionType.RESTAKE_REWARDS -> { - resourceReference(R.string.staking_restake) to - resourceReference(R.string.staking_notification_restake_rewards_text) - } - StakingActionType.WITHDRAW -> { - resourceReference(R.string.staking_withdraw) to - resourceReference(R.string.staking_notification_withdraw_text) - } - StakingActionType.UNLOCK_LOCKED -> { - val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days - if (cooldownPeriodDays != null) { - resourceReference(R.string.staking_unlocked_locked) to resourceReference( - R.string.staking_notification_unlock_text, - wrappedList( - pluralReference( - id = R.plurals.common_days, - count = cooldownPeriodDays, - formatArgs = wrappedList(cooldownPeriodDays), - ), - ), - ) - } else { - null to null - } - } - StakingActionType.VOTE_LOCKED -> { - resourceReference(R.string.staking_revote) to - resourceReference(R.string.staking_notifications_revote_tron_text) - } - else -> null to null - } - - if (titleReference != null && textReference != null) { - add( - StakingNotification.Info.Ordinary( - title = titleReference, - text = textReference, - ), - ) - } - } - - private fun MutableList.addTronRevoteNotification() { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val isTron = isTron(cryptoCurrencyStatus.currency.network.id.value) - val hasStakedBalance = (cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data)?.balance - ?.items?.any { - it.type == BalanceType.PREPARING || - it.type == BalanceType.STAKED || - it.type == BalanceType.LOCKED - } == true - if (isTron && hasStakedBalance) { - add( - StakingNotification.Info.Ordinary( - title = resourceReference(R.string.staking_revote), - text = resourceReference(R.string.staking_notifications_revote_tron_text), - ), - ) - } - } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissStakingNotificationsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/DismissStakingNotificationsStateTransformer.kt similarity index 98% rename from features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissStakingNotificationsStateTransformer.kt rename to features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/DismissStakingNotificationsStateTransformer.kt index a5fc4d9bc5..ba9f953c0d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissStakingNotificationsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/DismissStakingNotificationsStateTransformer.kt @@ -1,4 +1,4 @@ -package com.tangem.features.staking.impl.presentation.state.transformers +package com.tangem.features.staking.impl.presentation.state.transformers.notifications import com.tangem.common.ui.notifications.NotificationUM import com.tangem.features.staking.impl.presentation.state.StakingStates diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt new file mode 100644 index 0000000000..11b61e92b9 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt @@ -0,0 +1,180 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.notifications + +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.extensions.pluralReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.staking.model.stakekit.BalanceType +import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.R +import com.tangem.features.staking.impl.presentation.state.StakingNotification +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.lib.crypto.BlockchainUtils.isCosmos +import com.tangem.lib.crypto.BlockchainUtils.isTron +import com.tangem.utils.Provider +import com.tangem.utils.extensions.isZero +import com.tangem.utils.extensions.orZero +import java.math.BigDecimal + +internal class StakingInfoNotificationsFactory( + private val cryptoCurrencyStatusProvider: Provider, + private val yield: Yield, + private val isSubtractAvailable: Boolean, +) { + + /** + * @param notifications current notification to display + * @param prevState current screen state to update + * @param sendingAmount amount being transferred from user account + * @param actionAmount any amount being transferred or used action + * @param feeValue fee amount payed from user account + */ + fun addInfoNotifications( + notifications: MutableList, + prevState: StakingUiState, + sendingAmount: BigDecimal, + actionAmount: BigDecimal, + feeValue: BigDecimal, + ) = with(notifications) { + addStakingLowBalanceNotification(prevState, actionAmount) + + when (prevState.actionType) { + StakingActionCommonType.Enter -> addEnterInfoNotifications(sendingAmount, feeValue) + StakingActionCommonType.Exit -> addExitInfoNotifications() + is StakingActionCommonType.Pending -> addPendingInfoNotifications(prevState) + } + } + + private fun MutableList.addExitInfoNotifications() { + val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days + if (cooldownPeriodDays != null) { + add( + StakingNotification.Info.Unstake( + cooldownPeriodDays = cooldownPeriodDays, + subtitleRes = if (isCosmos(cryptoCurrencyStatusProvider().currency.network.id.value)) { + R.string.staking_notification_unstake_cosmos_text + } else { + R.string.staking_notification_unstake_text + }, + ), + ) + } + } + + private fun MutableList.addEnterInfoNotifications( + sendingAmount: BigDecimal, + feeValue: BigDecimal, + ) { + addTronRevoteNotification() + addStakingEntireBalanceNotification(sendingAmount, feeValue) + } + + private fun MutableList.addPendingInfoNotifications(prevState: StakingUiState) { + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + val pendingActionType = confirmationState?.pendingAction?.type + val (titleReference, textReference) = when (pendingActionType) { + StakingActionType.CLAIM_REWARDS -> { + resourceReference(R.string.common_claim) to + resourceReference(R.string.staking_notification_claim_rewards_text) + } + StakingActionType.RESTAKE_REWARDS -> { + resourceReference(R.string.staking_restake) to + resourceReference(R.string.staking_notification_restake_rewards_text) + } + StakingActionType.WITHDRAW -> { + resourceReference(R.string.staking_withdraw) to + resourceReference(R.string.staking_notification_withdraw_text) + } + StakingActionType.UNLOCK_LOCKED -> { + val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days + if (cooldownPeriodDays != null) { + resourceReference(R.string.staking_unlocked_locked) to resourceReference( + R.string.staking_notification_unlock_text, + wrappedList( + pluralReference( + id = R.plurals.common_days, + count = cooldownPeriodDays, + formatArgs = wrappedList(cooldownPeriodDays), + ), + ), + ) + } else { + null to null + } + } + StakingActionType.VOTE_LOCKED -> { + resourceReference(R.string.staking_revote) to + resourceReference(R.string.staking_notifications_revote_tron_text) + } + StakingActionType.RESTAKE -> { + resourceReference(R.string.staking_restake) to + resourceReference(R.string.staking_notification_restake_text) + } + else -> null to null + } + + if (titleReference != null && textReference != null) { + add( + StakingNotification.Info.Ordinary( + title = titleReference, + text = textReference, + ), + ) + } + } + + private fun MutableList.addTronRevoteNotification() { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val isTron = isTron(cryptoCurrencyStatus.currency.network.id.value) + val hasStakedBalance = (cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data)?.balance + ?.items?.any { + it.type == BalanceType.PREPARING || + it.type == BalanceType.STAKED || + it.type == BalanceType.LOCKED + } == true + if (isTron && hasStakedBalance) { + add( + StakingNotification.Info.Ordinary( + title = resourceReference(R.string.staking_revote), + text = resourceReference(R.string.staking_notifications_revote_tron_text), + ), + ) + } + } + + private fun MutableList.addStakingEntireBalanceNotification( + sendingAmount: BigDecimal, + feeValue: BigDecimal, + ) { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val balance = cryptoCurrencyStatus.value.amount.orZero() + + val isEntireBalance = sendingAmount.plus(feeValue) == balance + + if (isEntireBalance && isSubtractAvailable) { + add(StakingNotification.Info.StakeEntireBalance) + } + } + + private fun MutableList.addStakingLowBalanceNotification( + prevState: StakingUiState, + actionAmount: BigDecimal, + ) { + if (prevState.actionType != StakingActionCommonType.Exit) return + + val maxAmount = prevState.balanceState?.cryptoAmount ?: return + val exitRequirements = yield.args.exit?.args?.get(Yield.Args.ArgType.AMOUNT) ?: return + + val amountLeft = maxAmount - actionAmount + val isNotEnoughLeft = !amountLeft.isZero() && amountLeft < exitRequirements.minimum.orZero() + + if (exitRequirements.required && isNotEnoughLeft) { + add(StakingNotification.Warning.LowStakedBalance) + } + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt index e6ba4e32db..0f79a3ab26 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/validator/ValidatorSelectChangeTransformer.kt @@ -1,29 +1,45 @@ package com.tangem.features.staking.impl.presentation.state.transformers.validator import com.tangem.domain.staking.model.stakekit.Yield +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState -import com.tangem.features.staking.impl.presentation.state.ValidatorState import com.tangem.utils.transformer.Transformer internal class ValidatorSelectChangeTransformer( + private val yield: Yield, private val selectedValidator: Yield.Validator?, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { - val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState - selectedValidator ?: return prevState - val validatorState = (confirmationState.validatorState as? ValidatorState.Content)?.copy( - chosenValidator = selectedValidator, - ) ?: ValidatorState.Content( - isClickable = false, - availableValidators = emptyList(), - chosenValidator = selectedValidator, - ) + val validatorState = prevState.validatorState as? StakingStates.ValidatorState.Data + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + + val isRestake = prevState.actionType == StakingActionCommonType.Pending.Restake + val isEnter = prevState.actionType == StakingActionCommonType.Enter + val isFromInfoScreen = prevState.currentStep == StakingStep.InitialInfo + val isVoteLocked = confirmationState?.pendingAction?.type == StakingActionType.VOTE_LOCKED + + val activeValidator = selectedValidator.takeIf { isFromInfoScreen && isRestake } + ?: validatorState?.activeValidator + val filteredValidators = yield.preferredValidators.filterNot { it == activeValidator } + + val selectedValidator = if (isRestake && isFromInfoScreen) { + filteredValidators.firstOrNull() + } else { + selectedValidator + } return prevState.copy( - confirmationState = confirmationState.copy( - validatorState = validatorState, + validatorState = StakingStates.ValidatorState.Data( + chosenValidator = selectedValidator ?: yield.preferredValidators.first(), + availableValidators = filteredValidators, + isPrimaryButtonEnabled = true, + isClickable = true, + activeValidator = activeValidator, + isVisibleOnConfirmation = isEnter || isRestake || isVoteLocked, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt index efda617b73..558aa4252e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/StakingPendingActionUtils.kt @@ -5,8 +5,12 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionType +import com.tangem.features.staking.impl.presentation.state.BalanceState +import com.tangem.lib.crypto.BlockchainUtils.isBSC import com.tangem.lib.crypto.BlockchainUtils.isSolana +import com.tangem.lib.crypto.BlockchainUtils.isTron import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList @Suppress("CyclomaticComplexMethod") internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (this) { @@ -29,8 +33,35 @@ internal fun StakingActionType?.getPendingActionTitle(): TextReference = when (t null -> TextReference.EMPTY } -internal fun isSolanaWithdraw(networkId: String, pendingActions: ImmutableList?): Boolean { - val isSolana = isSolana(networkId) - val isWithdraw = pendingActions?.all { it.type == StakingActionType.WITHDRAW } == true - return isSolana && isWithdraw && !pendingActions.isNullOrEmpty() +internal fun isSingleAction(networkId: String, activeStake: BalanceState): Boolean { + val isSingleAction = activeStake.pendingActions.size <= 1 // Either single or none pending actions + val isCompositePendingActions = isCompositePendingActions(networkId, activeStake.pendingActions) + val isBscRestake = isBSC(networkId) && activeStake.pendingActions.any { + it.type == StakingActionType.RESTAKE + } + + return isSingleAction && !isBscRestake || isCompositePendingActions +} + +internal fun withStubUnstakeAction(networkId: String, activeStake: BalanceState) = if (isBSC(networkId)) { + activeStake.pendingActions.plus( + PendingAction( + type = StakingActionType.UNSTAKE, + passthrough = "", + args = null, + ), + ).toPersistentList() +} else { + activeStake.pendingActions +} + +internal fun isTronStakedBalance(networkId: String, pendingAction: PendingAction?): Boolean { + return isTron(networkId) && pendingAction?.type == StakingActionType.REVOTE +} + +internal fun isCompositePendingActions(networkId: String, pendingActions: ImmutableList?): Boolean { + return when { + isSolana(networkId) -> pendingActions?.any { it.type == StakingActionType.WITHDRAW } == true + else -> false + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt index 2068357514..180306b99e 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingClaimRewardsValidatorContent.kt @@ -12,8 +12,9 @@ import androidx.compose.ui.Modifier import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents @@ -41,16 +42,13 @@ internal fun StakingClaimRewardsValidatorContent( annotatedReference { appendSpace() appendColored( - text = BigDecimalFormatter.formatPercent( - percent = item.validator?.apr.orZero(), - useAbsoluteValue = true, - ), + text = item.validator?.apr.orZero().format { percent() }, color = TangemTheme.colors.text.accent, ) }, ), - infoTitle = item.fiatAmount, - infoSubtitle = item.cryptoAmount, + infoTitle = item.formattedFiatAmount, + infoSubtitle = item.formattedCryptoAmount, imageUrl = item.validator?.image.orEmpty(), onImageError = { ValidatorImagePlaceholder() }, modifier = modifier diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index 255fd36faa..2df8cdd525 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -19,29 +19,31 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.TransactionDoneState import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData +import com.tangem.features.staking.impl.presentation.state.previewdata.ValidatorStatePreviewData import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock import com.tangem.features.staking.impl.presentation.ui.block.ValidatorBlock import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +@Suppress("LongParameterList") @Composable internal fun StakingConfirmationContent( amountState: AmountState, state: StakingStates.ConfirmationState, + validatorState: StakingStates.ValidatorState, clickIntents: StakingClickIntents, type: StakingActionCommonType, + isSolana: Boolean, // TODO staking [REDACTED_TASK_KEY] support solana multisize hashes signing ) { if (state !is StakingStates.ConfirmationState.Data) return - val isEnterAction = type == StakingActionCommonType.ENTER - val showValidatorBlock = isEnterAction || state.pendingAction?.type == StakingActionType.VOTE_LOCKED + val isAmountEditable = type == StakingActionCommonType.Enter || type == StakingActionCommonType.Exit && !isSolana val isTransactionSent = state.innerState == InnerConfirmationStakingState.COMPLETED val isTransactionInProgress = state.notifications.any { it is StakingNotification.Warning.TransactionInProgress } Column( @@ -63,17 +65,15 @@ internal fun StakingConfirmationContent( } AmountBlock( amountState = amountState, - isClickDisabled = !isEnterAction || isTransactionSent || isTransactionInProgress, - isEditingDisabled = !isEnterAction && state.innerState != InnerConfirmationStakingState.COMPLETED, + isClickDisabled = !isAmountEditable || isTransactionSent || isTransactionInProgress, + isEditingDisabled = !isAmountEditable && state.innerState != InnerConfirmationStakingState.COMPLETED, onClick = clickIntents::onPrevClick, ) - if (showValidatorBlock) { - ValidatorBlock( - validatorState = state.validatorState, - isClickable = !isTransactionInProgress, - onClick = clickIntents::openValidators, - ) - } + ValidatorBlock( + validatorState = validatorState, + isClickable = !isTransactionInProgress, + onClick = clickIntents::openValidators, + ) StakingFeeBlock(feeState = state.feeState, isTransactionSent = isTransactionSent) NotificationsBlock(notifications = state.notifications) } @@ -88,8 +88,10 @@ private fun Preview_StakingConfirmationContent() { StakingConfirmationContent( amountState = AmountStatePreviewData.amountState, state = ConfirmationStatePreviewData.assentStakingState, + validatorState = ValidatorStatePreviewData.validatorState, clickIntents = StakingClickIntentsStub, - type = StakingActionCommonType.ENTER, + type = StakingActionCommonType.Enter, + isSolana = false, ) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index 4a5d106633..95038b16d9 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -41,11 +41,12 @@ import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.components.list.roundedListWithDividersItems import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.features.staking.impl.R @@ -127,18 +128,20 @@ private fun LazyListScope.activeStakingBlock( clickIntents: StakingClickIntents, isBalanceHidden: Boolean, ) { - val balances = (state.yieldBalance as? InnerYieldBalanceState.Data)?.balance - if (!balances.isNullOrEmpty()) { - item(key = STAKING_REWARD_BLOCK_KEY) { - Column(modifier = Modifier.animateItem()) { - StakingRewardBlock( - yieldBalanceState = state.yieldBalance, - onRewardsClick = clickIntents::openRewardsValidators, - isBalanceHidden = isBalanceHidden, - ) - SpacerH12() - } + val innerYieldBalanceState = state.yieldBalance as? InnerYieldBalanceState.Data ?: return + + item(key = STAKING_REWARD_BLOCK_KEY) { + Column(modifier = Modifier.animateItem()) { + StakingRewardBlock( + yieldBalanceState = state.yieldBalance, + onRewardsClick = clickIntents::openRewardsValidators, + isBalanceHidden = isBalanceHidden, + ) + SpacerH12() } + } + + if (innerYieldBalanceState.balances.isNotEmpty()) { item(ACTIVE_STAKING_BLOCK_KEY) { Text( text = stringResource(id = R.string.staking_your_stakes), @@ -147,7 +150,7 @@ private fun LazyListScope.activeStakingBlock( modifier = Modifier .roundedShapeItemDecoration( currentIndex = 0, - lastIndex = 1 + state.yieldBalance.balance.lastIndex, + lastIndex = 1 + state.yieldBalance.balances.lastIndex, addDefaultPadding = false, ) .fillMaxWidth() @@ -161,10 +164,14 @@ private fun LazyListScope.activeStakingBlock( ) } itemsIndexed( - items = state.yieldBalance.balance, - key = { _, balance -> + items = state.yieldBalance.balances, + key = { index, balance -> // Staked balance does not have unique identifier. - balance.toString() + buildString { + append(balance.hashCode()) + append("_") + append(index) + } }, ) { index, balance -> ActiveStakingBlock( @@ -176,7 +183,7 @@ private fun LazyListScope.activeStakingBlock( .animateItem() .roundedShapeItemDecoration( currentIndex = index + 1, - lastIndex = state.yieldBalance.balance.lastIndex + 1, + lastIndex = state.yieldBalance.balances.lastIndex + 1, addDefaultPadding = false, ), ) @@ -269,8 +276,8 @@ private fun ActiveStakingBlock( InputRowImageInfo( subtitle = balance.title, caption = balance.subtitle ?: balance.getAprText(), - infoTitle = balance.fiatAmount.orMaskWithStars(isBalanceHidden), - infoSubtitle = balance.cryptoAmount.orMaskWithStars(isBalanceHidden), + infoTitle = balance.formattedFiatAmount.orMaskWithStars(isBalanceHidden), + infoSubtitle = balance.formattedCryptoAmount.orMaskWithStars(isBalanceHidden), imageUrl = balance.getImage(), iconRes = icon, iconTint = iconTint, @@ -311,10 +318,7 @@ private fun BalanceState.getAprText() = combinedReference( annotatedReference { appendSpace() appendColored( - text = BigDecimalFormatter.formatPercent( - percent = validator?.apr.orZero(), - useAbsoluteValue = true, - ), + text = validator?.apr.orZero().format { percent() }, color = TangemTheme.colors.text.accent, ) }, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 5c2e5ceb51..8e6b03d04c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -3,7 +3,6 @@ package com.tangem.features.staking.impl.presentation.ui import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.tween import androidx.compose.animation.togetherWith import androidx.compose.foundation.background @@ -29,6 +28,7 @@ import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingAc import com.tangem.features.staking.impl.presentation.state.bottomsheet.StakingInfoBottomSheetConfig import com.tangem.features.staking.impl.presentation.ui.bottomsheet.StakingActionSelectorBottomSheet import com.tangem.features.staking.impl.presentation.ui.bottomsheet.StakingInfoBottomSheet +import com.tangem.lib.crypto.BlockchainUtils.isSolana import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map @@ -89,15 +89,12 @@ private fun StakingAppBar(uiState: StakingUiState) { val (backIcon, click) = when (uiState.currentStep) { StakingStep.Amount, StakingStep.Confirmation, - -> { - R.drawable.ic_close_24 to uiState.clickIntents::onBackClick - } + -> R.drawable.ic_close_24 to uiState.clickIntents::onBackClick StakingStep.Validators, StakingStep.RewardsValidators, + StakingStep.RestakeValidator, StakingStep.InitialInfo, - -> { - R.drawable.ic_back_24 to uiState.clickIntents::onPrevClick - } + -> R.drawable.ic_back_24 to uiState.clickIntents::onPrevClick } AppBarWithBackButtonAndIcon( text = uiState.title.resolveReference(), @@ -109,7 +106,6 @@ private fun StakingAppBar(uiState: StakingUiState) { ) } -@OptIn(ExperimentalAnimationApi::class) @Composable private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = Modifier) { val currentScreen = uiState.currentStep @@ -172,17 +168,17 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M StakingStep.Confirmation -> StakingConfirmationContent( amountState = uiState.amountState, state = uiState.confirmationState, + validatorState = uiState.validatorState, clickIntents = uiState.clickIntents, type = uiState.actionType, + isSolana = isSolana(uiState.cryptoCurrencyBlockchainId), + ) + StakingStep.RestakeValidator, + StakingStep.Validators, + -> StakingValidatorListContent( + state = uiState.validatorState, + clickIntents = uiState.clickIntents, ) - StakingStep.Validators -> { - val confirmState = uiState.confirmationState - if (confirmState !is StakingStates.ConfirmationState.Data) return@AnimatedContent - StakingValidatorListContent( - state = confirmState.validatorState, - clickIntents = uiState.clickIntents, - ) - } } } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt index 9ddda86f32..1bcd5c9c60 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingValidatorListContent.kt @@ -24,12 +24,13 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.inputrow.InputRowImageSelector import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.ValidatorState -import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.previewdata.ValidatorStatePreviewData import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import com.tangem.utils.extensions.orZero @@ -39,7 +40,7 @@ import com.tangem.utils.extensions.orZero */ @Composable internal fun StakingValidatorListContent( - state: ValidatorState, + state: StakingStates.ValidatorState, clickIntents: StakingClickIntents, modifier: Modifier = Modifier, ) { @@ -51,7 +52,7 @@ internal fun StakingValidatorListContent( .background(TangemTheme.colors.background.secondary) .padding(horizontal = TangemTheme.dimens.spacing16), ) { - if (state is ValidatorState.Content) { + if (state is StakingStates.ValidatorState.Data) { val validators = state.availableValidators items( count = validators.size, @@ -67,7 +68,7 @@ internal fun StakingValidatorListContent( annotatedReference { appendSpace() appendColored( - text = BigDecimalFormatter.formatPercent(item.apr.orZero(), true), + text = item.apr.orZero().format { percent() }, color = TangemTheme.colors.text.accent, ) }, @@ -131,7 +132,7 @@ private fun RowScope.ValidatorLabel(isStrategicPartner: Boolean) { @Composable private fun StakingValidatorListContent_Preview( @PreviewParameter(StakingValidatorListContentPreviewProvider::class) - data: ValidatorState, + data: StakingStates.ValidatorState, ) { TangemThemePreview { StakingValidatorListContent( @@ -141,8 +142,8 @@ private fun StakingValidatorListContent_Preview( } } -private class StakingValidatorListContentPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf(ConfirmationStatePreviewData.assentStakingState.validatorState) +private class StakingValidatorListContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf(ValidatorStatePreviewData.validatorState) } // endregion \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index e666de9908..8817faede7 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -21,6 +21,9 @@ import com.tangem.common.ui.amountScreen.utils.getFiatReference import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.rows.SelectorRowItem import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fee +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.BigDecimalFormatter @@ -56,12 +59,12 @@ internal fun StakingFeeBlock(feeState: FeeState, isTransactionSent: Boolean) { titleRes = R.string.common_fee_selector_option_market, iconRes = R.drawable.ic_bird_24, preDot = stringReference( - BigDecimalFormatter.formatCryptoFeeAmount( - cryptoAmount = feeAmount?.value, - cryptoCurrency = feeAmount?.currencySymbol.orEmpty(), - decimals = feeAmount?.decimals ?: 0, - canBeLower = feeState.isFeeApproximate, - ), + feeAmount?.value.format { + crypto( + symbol = feeAmount?.currencySymbol.orEmpty(), + decimals = feeAmount?.decimals ?: 0, + ).fee(canBeLower = feeState.isFeeApproximate) + }, ), postDot = if (feeState.isFeeConvertibleToFiat) { getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt index 192d1e5397..3d5cc0310a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/ValidatorBlock.kt @@ -12,42 +12,44 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import com.tangem.core.ui.components.inputrow.InputRowImageInfo import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.ValidatorState +import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.ui.ValidatorImagePlaceholder import com.tangem.utils.extensions.orZero @Composable -internal fun ValidatorBlock(validatorState: ValidatorState, isClickable: Boolean, onClick: () -> Unit) { +internal fun ValidatorBlock(validatorState: StakingStates.ValidatorState, isClickable: Boolean, onClick: () -> Unit) { + val state = validatorState as? StakingStates.ValidatorState.Data ?: return + if (!state.isVisibleOnConfirmation) return + Column( modifier = Modifier .fillMaxWidth() .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action) .clickable( - enabled = validatorState.isClickable && isClickable, + enabled = state.isClickable && isClickable, interactionSource = remember { MutableInteractionSource() }, indication = ripple(), onClick = onClick, ), ) { - if (validatorState is ValidatorState.Content) { - InputRowImageInfo( - title = resourceReference(R.string.staking_validator), - subtitle = stringReference(validatorState.chosenValidator.name), - infoTitle = annotatedReference { - append(resourceReference(R.string.staking_details_apr).resolveReference()) - appendSpace() - appendColored( - text = BigDecimalFormatter.formatPercent(validatorState.chosenValidator.apr.orZero(), true), - color = TangemTheme.colors.text.accent, - ) - }, - imageUrl = validatorState.chosenValidator.image.orEmpty(), - onImageError = { ValidatorImagePlaceholder() }, - ) - } + InputRowImageInfo( + title = resourceReference(R.string.staking_validator), + subtitle = stringReference(state.chosenValidator.name), + infoTitle = annotatedReference { + append(resourceReference(R.string.staking_details_apr).resolveReference()) + appendSpace() + appendColored( + text = state.chosenValidator.apr.orZero().format { percent() }, + color = TangemTheme.colors.text.accent, + ) + }, + imageUrl = state.chosenValidator.image.orEmpty(), + onImageError = { ValidatorImagePlaceholder() }, + ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index 8e55492aa8..1b4d3eeee4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -3,13 +3,10 @@ package com.tangem.features.staking.impl.presentation.viewmodel import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType -import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @Suppress("TooManyFunctions") @@ -17,12 +14,7 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onBackClick() - fun onNextClick( - actionTypeToOverwrite: StakingActionCommonType? = null, - pendingAction: PendingAction? = null, - pendingActions: ImmutableList? = null, - balanceState: BalanceState? = null, - ) + fun onNextClick(balanceState: BalanceState? = null) fun onActionClick() @@ -34,11 +26,11 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onInfoClick(infoType: InfoType) - fun onEnterClick() + fun onAmountEnterClick() - fun getFee(pendingAction: PendingAction?, pendingActions: ImmutableList?) + fun getFee() - override fun onAmountNext() = onNextClick(actionTypeToOverwrite = null) + override fun onAmountNext() = onNextClick() fun openValidators() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 40cc866b89..522d616f07 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -11,6 +11,7 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig import com.tangem.common.ui.notifications.NotificationUM @@ -26,14 +27,12 @@ import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.staking.GetActionsUseCase import com.tangem.domain.staking.InvalidatePendingTransactionsUseCase import com.tangem.domain.staking.IsAnyTokenStakedUseCase import com.tangem.domain.staking.IsApproveNeededUseCase import com.tangem.domain.staking.model.StakingApproval -import com.tangem.domain.staking.model.stakekit.BalanceItem -import com.tangem.domain.staking.model.stakekit.PendingAction -import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.tokens.* @@ -50,6 +49,8 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.staking.analytics.StakeScreenSource import com.tangem.domain.staking.analytics.StakingAnalyticsEvent +import com.tangem.domain.staking.model.stakekit.* +import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.features.staking.impl.analytics.StakingParamsInterceptor import com.tangem.features.staking.impl.analytics.utils.StakingAnalyticSender import com.tangem.features.staking.impl.navigation.InnerStakingRouter @@ -64,13 +65,19 @@ import com.tangem.features.staking.impl.presentation.state.helpers.StakingTransa import com.tangem.features.staking.impl.presentation.state.transformers.* import com.tangem.features.staking.impl.presentation.state.transformers.amount.* import com.tangem.features.staking.impl.presentation.state.transformers.approval.* +import com.tangem.features.staking.impl.presentation.state.transformers.confirmation.SetUpdatedAllowanceTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.notifications.AddStakingNotificationsTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.notifications.DismissStakingNotificationsStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer -import com.tangem.features.staking.impl.presentation.state.utils.isSolanaWithdraw +import com.tangem.features.staking.impl.presentation.state.utils.isSingleAction +import com.tangem.features.staking.impl.presentation.state.utils.withStubUnstakeAction import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.extensions.isSingleItem +import com.tangem.utils.extensions.orZero import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -88,6 +95,7 @@ internal class StakingViewModel @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, + private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val sendTransactionUseCase: SendTransactionUseCase, @@ -108,6 +116,7 @@ internal class StakingViewModel @Inject constructor( private val stakingBalanceUpdater: StakingBalanceUpdater.Factory, private val analyticsEventHandler: AnalyticsEventHandler, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val getActionsUseCase: GetActionsUseCase, private val paramsInterceptorHolder: ParamsInterceptorHolder, @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, @@ -132,7 +141,9 @@ internal class StakingViewModel @Inject constructor( ?: error("This screen can't be opened without `Yield`") private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull() + private var processingActions: List = emptyList() private var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null + private var minimumTransactionAmount: EnterAmountBoundary? = null private var innerRouter: InnerStakingRouter by Delegates.notNull() private var userWallet: UserWallet by Delegates.notNull() @@ -142,9 +153,8 @@ internal class StakingViewModel @Inject constructor( get() { val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data return invalidatePendingTransactionsUseCase( - userWalletId = userWallet.walletId, balanceItems = yieldBalance?.balance?.items ?: emptyList(), - balancesId = yieldBalance?.getBalancesUniqueId() ?: 0, + processingActions = processingActions, ).getOrElse { emptyList() } } @@ -154,6 +164,7 @@ internal class StakingViewModel @Inject constructor( stakingBalanceUpdater.create( cryptoCurrencyStatus, userWallet, + yield, ) } @@ -162,7 +173,6 @@ internal class StakingViewModel @Inject constructor( cryptoCurrencyStatus = cryptoCurrencyStatus, userWallet = userWallet, yield = yield, - stakingApproval = stakingApproval, ) } @@ -187,7 +197,9 @@ internal class StakingViewModel @Inject constructor( ) private var stakingApproval: StakingApproval = StakingApproval.Empty + private var stakingAllowance: BigDecimal = BigDecimal.ZERO private var isAmountSubtractAvailable: Boolean = false + private var isAnyTokenStaked: Boolean = false private val allowanceTaskScheduler = SingleTaskScheduler() private val transactionsInProgress: CopyOnWriteArrayList = CopyOnWriteArrayList() @@ -195,6 +207,7 @@ internal class StakingViewModel @Inject constructor( private var approvalJobHolder: JobHolder = JobHolder() private var feeJobHolder: JobHolder = JobHolder() private var sendTransactionJobHolder = JobHolder() + private var stepChangesJobHolder = JobHolder() init { subscribeOnSelectedAppCurrency() @@ -208,76 +221,30 @@ internal class StakingViewModel @Inject constructor( approvalJobHolder.cancel() feeJobHolder.cancel() sendTransactionJobHolder.cancel() + stepChangesJobHolder.cancel() } override fun onBackClick() { stakingStateRouter.onBackClick() } - override fun onNextClick( - actionTypeToOverwrite: StakingActionCommonType?, - pendingAction: PendingAction?, - pendingActions: ImmutableList?, - balanceState: BalanceState?, - ) { - if (actionTypeToOverwrite != null) { - stateController.update(SetActionToExecuteTransformer(actionTypeToOverwrite, pendingAction, pendingActions)) + override fun onNextClick(balanceState: BalanceState?) { + if (value.currentStep == StakingStep.InitialInfo && balanceState == null) { + stateController.update( + SetConfirmationStateInitTransformer( + isEnter = true, + isExplicitExit = false, + balanceState = null, + cryptoCurrencyStatus = cryptoCurrencyStatus, + stakingApproval = stakingApproval, + stakingAllowance = stakingAllowance, + ), + ) } stakingStateRouter.onNextClick() - when { - isInitState() -> { - stateController.update(SetConfirmationStateResetAssentTransformer) - stateController.update( - SetConfirmationStateLoadingTransformer( - yield = yield, - appCurrency = appCurrency, - cryptoCurrency = cryptoCurrencyStatus.currency, - ), - ) - if (balanceState != null) { - stateController.update( - SetPossiblePendingTransactionTransformer(yield, balanceState, cryptoCurrencyStatus), - ) - } - - stateController.update( - transformer = SetInitialDataStateTransformer( - clickIntents = this@StakingViewModel, - yield = yield, - isAnyTokenStaked = true, - isApprovalNeeded = stakingApproval is StakingApproval.Needed, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - userWalletProvider = Provider { userWallet }, - appCurrencyProvider = Provider { appCurrency }, - balancesToShowProvider = Provider { balancesToShow }, - ), - ) - onRefreshSwipe(isRefreshing = false) - } - isAssentState() -> { - getFee(pendingAction, pendingActions) - if (balanceState != null) { - stateController.update( - SetPossiblePendingTransactionTransformer(yield, balanceState, cryptoCurrencyStatus), - ) - } - val amountState = value.amountState as? AmountState.Data - if (amountState?.amountTextField?.isWarning == true) { - stateController.update( - AmountRoundToIntegerTransformer( - cryptoCurrencyStatus = cryptoCurrencyStatus, - ), - ) - } - } - } } - override fun onActionClick() { - handleOnNextConfirmationClick() - } - - override fun getFee(pendingAction: PendingAction?, pendingActions: ImmutableList?) { + override fun getFee() { stateController.update( SetConfirmationStateLoadingTransformer( yield = yield, @@ -287,16 +254,12 @@ internal class StakingViewModel @Inject constructor( ) viewModelScope.launch { feeLoader.getFee( - pendingAction = pendingAction, - pendingActions = pendingActions, onStakingFee = { gasEstimate -> stateController.update( SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = gasEstimate, - action = pendingAction, - actions = pendingActions, ), ) updateNotifications() @@ -324,7 +287,7 @@ internal class StakingViewModel @Inject constructor( }.saveIn(feeJobHolder) } - private fun handleOnNextConfirmationClick() { + override fun onActionClick() { if (isAssentState()) { viewModelScope.launch { stakingAnalyticSender.sendTransactionStakingClickedAnalytics(value) @@ -343,7 +306,6 @@ internal class StakingViewModel @Inject constructor( stateController.update(SetConfirmationStateCompletedTransformer(txUrl)) }, onSendError = { error -> - Timber.e(error.toString()) analyticsEventHandler.send(StakingAnalyticsEvent.TransactionError) stakingEventFactory.createSendTransactionErrorAlert(error) stateController.update(SetConfirmationStateResetAssentTransformer) @@ -383,7 +345,7 @@ internal class StakingViewModel @Inject constructor( override fun onRefreshSwipe(isRefreshing: Boolean) { stateController.update(SetInitialLoadingStateTransformer(isRefreshing)) coroutineScope.launch { - balanceUpdater.instantUpdate() + balanceUpdater.partialUpdate() }.invokeOnCompletion { stateController.update(SetInitialLoadingStateTransformer(false)) } @@ -402,34 +364,33 @@ internal class StakingViewModel @Inject constructor( ) } - override fun onEnterClick() { - onAmountValueChange("") // reset amount state - - // TODO refactor in [REDACTED_JIRA] - val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data - val filteredValidator = yield.validators.filter { it.preferred } - if (filteredValidator.isEmpty()) { + override fun onAmountEnterClick() { + if (yield.preferredValidators.isEmpty()) { stateController.updateEvent( StakingEvent.ShowAlert(StakingAlertUM.NoAvailableValidators), ) } else { - stateController.update { - value.copy( - confirmationState = confirmationState?.copy( - validatorState = ValidatorState.Content( - isClickable = true, - chosenValidator = filteredValidator[0], - availableValidators = filteredValidator, - ), - ) as StakingStates.ConfirmationState, + if (uiState.value.actionType == StakingActionCommonType.Enter) { + stateController.updateAll( + ValidatorSelectChangeTransformer( + selectedValidator = null, + yield = yield, + ), ) } - onNextClick(StakingActionCommonType.ENTER) + onNextClick() } } override fun onAmountValueChange(value: String) { - stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, value, yield)) + stateController.update( + AmountChangeStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minimumTransactionAmount, + value = value, + yield = yield, + ), + ) } override fun onAmountPasteTriggerDismiss() { @@ -438,7 +399,14 @@ internal class StakingViewModel @Inject constructor( override fun onMaxValueClick() { analyticsEventHandler.send(StakingAnalyticsEvent.ButtonMax) - stateController.update(AmountMaxValueStateTransformer(cryptoCurrencyStatus, yield)) + stateController.update( + AmountMaxValueStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minimumTransactionAmount, + actionType = uiState.value.actionType, + yield = yield, + ), + ) } override fun onCurrencyChangeClick(isFiat: Boolean) { @@ -463,7 +431,12 @@ internal class StakingViewModel @Inject constructor( validator = validator.name, ), ) - stateController.update(ValidatorSelectChangeTransformer(validator)) + stateController.update( + ValidatorSelectChangeTransformer( + selectedValidator = validator, + yield = yield, + ), + ) } override fun openRewardsValidators() { @@ -479,50 +452,42 @@ internal class StakingViewModel @Inject constructor( source = StakeScreenSource.Info, ), ) - onNextClick(actionTypeToOverwrite = StakingActionCommonType.PENDING_REWARDS) + stateController.update { + value.copy(actionType = StakingActionCommonType.Pending.Rewards) + } + stakingStateRouter.showRewardsValidators() } } override fun onActiveStake(activeStake: BalanceState) { - val isAllWithdrawActions = isSolanaWithdraw( - cryptoCurrencyStatus.currency.network.id.value, - activeStake.pendingActions, - ) - val isMultiplePendingActions = activeStake.pendingActions.size > 1 - if (isMultiplePendingActions && !isAllWithdrawActions) { + val networkId = cryptoCurrencyStatus.currency.network.id.value + if (isSingleAction(networkId, activeStake)) { + prepareForConfirmation( + balanceType = activeStake.type, + pendingActions = activeStake.pendingActions, + balanceState = activeStake, + validator = activeStake.validator, + amountValue = activeStake.cryptoValue, + ) + onNextClick(activeStake) + } else { stateController.update( ShowActionSelectorBottomSheetTransformer( - pendingActions = activeStake.pendingActions, + pendingActions = withStubUnstakeAction(networkId, activeStake), onActionSelect = { action -> - stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) - stateController.update( - AmountChangeStateTransformer( - cryptoCurrencyStatus, - activeStake.cryptoValue, - yield, - ), - ) - onNextClick( - actionTypeToOverwrite = StakingActionCommonType.PENDING_OTHER, + prepareForConfirmation( + balanceType = activeStake.type, pendingAction = action, balanceState = activeStake, + validator = activeStake.validator, + amountValue = activeStake.cryptoValue, ) stateController.update(DismissBottomSheetStateTransformer) + onNextClick(activeStake) }, onDismiss = { stateController.update(DismissBottomSheetStateTransformer) }, ), ) - } else { - stateController.update(ActionTypeActiveStakeTransformer(cryptoCurrencyStatus, activeStake)) - stateController.update(ValidatorSelectChangeTransformer(activeStake.validator)) - stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue, yield)) - - onNextClick( - actionTypeToOverwrite = null, - pendingAction = activeStake.pendingActions.firstOrNull(), - pendingActions = activeStake.pendingActions.takeIf { isAllWithdrawActions }, - balanceState = activeStake, - ) } } @@ -617,7 +582,7 @@ internal class StakingViewModel @Inject constructor( stakingAnalyticSender.sendTransactionApprovalAnalytics(tokenCryptoCurrency) stateController.update(SetApprovalInProgressTransformer) stateController.update(DismissBottomSheetStateTransformer) - awaitForAllowance(confirmationState.pendingAction) + awaitForAllowance() }, ) }.saveIn(approvalJobHolder) @@ -681,6 +646,7 @@ internal class StakingViewModel @Inject constructor( ) { AmountReduceByStateTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minimumTransactionAmount, value = AmountReduceByTransformer.ReduceByData( reduceAmountBy = reduceAmountBy, reduceAmountByDiff = reduceAmountByDiff, @@ -693,6 +659,7 @@ internal class StakingViewModel @Inject constructor( stateController.update( AmountReduceToStateTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, + minimumTransactionAmount = minimumTransactionAmount, value = reduceAmountTo, ), ) @@ -703,7 +670,7 @@ internal class StakingViewModel @Inject constructor( stateController.update(DismissStakingNotificationsStateTransformer(notification)) } - private fun awaitForAllowance(pendingAction: PendingAction?) { + private fun awaitForAllowance() { val approval = stakingApproval as? StakingApproval.Needed ?: return allowanceTaskScheduler.scheduleTask( scope = viewModelScope, @@ -719,10 +686,12 @@ internal class StakingViewModel @Inject constructor( } }, onSuccess = { allowance -> + stakingAllowance = allowance val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value ?: error("No amount provided") if (allowance >= amount) { - getFee(pendingAction = pendingAction, pendingActions = null) + stateController.update(SetUpdatedAllowanceTransformer(allowance)) + getFee() allowanceTaskScheduler.cancelTask() } }, @@ -760,7 +729,7 @@ internal class StakingViewModel @Inject constructor( val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrElse { error("CardInfo must be not null") } val amountState = uiState.value.amountState as? AmountState.Data val confirmationState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data - val validatorState = confirmationState?.validatorState as? ValidatorState.Content + val validatorState = uiState.value.validatorState as? StakingStates.ValidatorState.Data val feeState = confirmationState?.feeState as? FeeState.Content val validator = validatorState?.chosenValidator @@ -798,8 +767,26 @@ internal class StakingViewModel @Inject constructor( this.stakingStateRouter = stateRouter } - private fun setupApprovalNeeded() { - stakingApproval = isApproveNeededUseCase(cryptoCurrencyStatus.currency).getOrElse { StakingApproval.Empty } + private suspend fun setupApprovalNeeded() { + stakingApproval = isApproveNeededUseCase(cryptoCurrencyStatus.currency).fold( + ifRight = { approval -> + if (approval is StakingApproval.Needed) { + stakingAllowance = getAllowanceUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + spenderAddress = approval.spenderAddress, + ).getOrElse { BigDecimal.ZERO } + } + approval + }, + ifLeft = { + StakingApproval.Empty + }, + ) + } + + private suspend fun setupIsAnyTokenStaked() { + isAnyTokenStaked = isAnyTokenStakedUseCase(userWalletId).getOrNull() ?: false } private fun subscribeOnCurrencyStatusUpdates() { @@ -808,7 +795,6 @@ internal class StakingViewModel @Inject constructor( userWallet = wallet }, ifLeft = { - Timber.e(it.toString()) stakingEventFactory.createGenericErrorAlert(it.toString()) stateController.update(SetConfirmationStateResetAssentTransformer) }, @@ -820,6 +806,8 @@ internal class StakingViewModel @Inject constructor( .onEach { maybeStatus -> maybeStatus.fold( ifRight = { status -> + if (status.value !is CryptoCurrencyStatus.Loaded) return@fold + if (!isInitialInfoAnalyticSent) { isInitialInfoAnalyticSent = true val balances = status.value.yieldBalance as? YieldBalance.Data @@ -835,27 +823,22 @@ internal class StakingViewModel @Inject constructor( feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull() + minimumTransactionAmount = + getMinimumTransactionAmountSyncUseCase(userWalletId, status).getOrNull()?.let { + EnterAmountBoundary( + amount = it, + fiatRate = status.value.fiatRate.orZero(), + ) + } cryptoCurrencyStatus = status - val isAnyTokenStaked = isAnyTokenStakedUseCase(userWalletId).getOrNull() ?: false setupApprovalNeeded() + setupIsAnyTokenStaked() checkIfSubtractAvailable() - - stateController.update( - transformer = SetInitialDataStateTransformer( - clickIntents = this@StakingViewModel, - yield = yield, - isAnyTokenStaked = isAnyTokenStaked, - isApprovalNeeded = stakingApproval is StakingApproval.Needed, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - userWalletProvider = Provider { userWallet }, - appCurrencyProvider = Provider { appCurrency }, - balancesToShowProvider = Provider { balancesToShow }, - ), - ) + subscribeOnActionsUpdates() + subscribeOnStepChanges() }, ifLeft = { error -> - Timber.e(error.toString()) stakingEventFactory.createGenericErrorAlert(error.toString()) stateController.update(SetConfirmationStateResetAssentTransformer) }, @@ -887,6 +870,109 @@ internal class StakingViewModel @Inject constructor( .launchIn(viewModelScope) } + private fun subscribeOnStepChanges() { + uiState + .distinctUntilChangedBy { it.currentStep } + .onEach { + when { + isInitState() -> { + updateInitialData() + balanceUpdater.initialUpdate() + } + isAssentState() -> { + getFee() + val amountState = value.amountState as? AmountState.Data + if (amountState?.amountTextField?.isWarning == true) { + stateController.update( + AmountRoundToIntegerTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + ), + ) + } + } + } + } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + .saveIn(stepChangesJobHolder) + } + + private fun subscribeOnActionsUpdates() { + getActionsUseCase( + userWalletId = userWalletId, + cryptoCurrencyId = cryptoCurrencyId, + ) + .conflate() + .distinctUntilChanged() + .onEach { result -> + result.getOrNull()?.let { actions -> + processingActions = actions + if (isInitState()) { + updateInitialData() + } + } + } + .flowOn(dispatchers.main) + .launchIn(viewModelScope) + } + + private fun updateInitialData() { + stateController.updateAll( + SetInitialDataStateTransformer( + clickIntents = this@StakingViewModel, + yield = yield, + isAnyTokenStaked = isAnyTokenStaked, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + userWalletProvider = Provider { userWallet }, + appCurrencyProvider = Provider { appCurrency }, + balancesToShowProvider = Provider { balancesToShow }, + ), + SetConfirmationStateEmptyTransformer, + ) + } + + private fun prepareForConfirmation( + balanceType: BalanceType, + balanceState: BalanceState, + pendingActions: ImmutableList = persistentListOf(), + pendingAction: PendingAction? = pendingActions.firstOrNull(), + validator: Yield.Validator?, + amountValue: String, + ) { + stateController.updateAll( + SetConfirmationStateInitTransformer( + isEnter = false, + isExplicitExit = isExplicitExit(balanceType, pendingAction), + balanceState = balanceState, + cryptoCurrencyStatus = cryptoCurrencyStatus, + stakingApproval = stakingApproval, + pendingActions = pendingActions, + pendingAction = pendingAction, + stakingAllowance = stakingAllowance, + ), + ValidatorSelectChangeTransformer( + selectedValidator = validator, + yield = yield, + ), + SetAmountDataTransformer( + clickIntents = this, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + userWalletProvider = Provider { userWallet }, + appCurrencyProvider = Provider { appCurrency }, + ), + AmountChangeStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + value = amountValue, + minimumTransactionAmount = minimumTransactionAmount, + yield = yield, + ), + ) + } + + private fun isExplicitExit(balanceType: BalanceType, pendingAction: PendingAction?): Boolean { + return balanceType == BalanceType.STAKED && pendingAction?.type != StakingActionType.RESTAKE + } + private fun isAssentState(): Boolean { return value.currentStep == StakingStep.Confirmation && (value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState == diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index ec5a88e56e..17850cb1ba 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -46,6 +46,7 @@ class SwapPairInfoConverter : Converter Fee.Common(feeAmount) // endregion } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index 390b8e7c7e..49a1dbc25b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -2,6 +2,8 @@ package com.tangem.feature.swap.converters import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency @@ -103,11 +105,9 @@ class TokensDataConverter( } private fun formatCryptoAmount(cryptoCurrencyStatus: CryptoCurrencyStatus): String { - return BigDecimalFormatter.formatCryptoAmount( - cryptoCurrencyStatus.value.amount, - cryptoCurrencyStatus.currency.symbol, - cryptoCurrencyStatus.currency.decimals, - ) + return cryptoCurrencyStatus.value.amount.format { + crypto(cryptoCurrencyStatus.currency) + } } private fun formatFiatAmount(cryptoCurrencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency): String { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapSingletonModule.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapSingletonModule.kt new file mode 100644 index 0000000000..d265dc06e2 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/SwapSingletonModule.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.swap.di + +import com.tangem.feature.swap.di.impl.DefaultAmountFormatter +import com.tangem.feature.swap.domain.models.ui.AmountFormatter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +class SwapSingletonModule { + + @Provides + fun provideAmountFormatter(): AmountFormatter { + return DefaultAmountFormatter() + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/impl/DefaultAmountFormatter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/impl/DefaultAmountFormatter.kt new file mode 100644 index 0000000000..91d8dd1821 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/di/impl/DefaultAmountFormatter.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.swap.di.impl + +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.ui.AmountFormatter +import java.math.BigDecimal + +class DefaultAmountFormatter : AmountFormatter { + + override fun formatSwapAmountToUI(swapAmount: SwapAmount, currency: String): String { + return swapAmount.value.format { crypto(symbol = currency, decimals = swapAmount.decimals) } + } + + override fun formatBigDecimalAmountToUI(amount: BigDecimal, decimals: Int, currency: String?): String { + return amount.format { crypto(symbol = currency.orEmpty(), decimals = decimals) } + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt index f99c9278ea..bb84996732 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt @@ -25,12 +25,12 @@ sealed class SwapAlertUM : AlertUM { resourceReference(id = R.string.common_support) } - data class FeesAlert( + data class InformationAlert( override val message: TextReference, override val onConfirmClick: (() -> Unit), ) : SwapAlertUM() { override val title: TextReference = resourceReference( - com.tangem.feature.swap.presentation.R.string.swapping_alert_title, + R.string.swapping_alert_title, ) override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 1e509b2280..e841fb0bba 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -10,7 +10,12 @@ import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.anyDecimals +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.uncapped import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -19,17 +24,18 @@ import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* -import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.* import com.tangem.feature.swap.models.states.events.SwapEvent import com.tangem.feature.swap.presentation.R +import com.tangem.feature.swap.utils.formatToUIRepresentation import com.tangem.feature.swap.utils.getExpressErrorMessage import com.tangem.feature.swap.utils.getExpressErrorTitle import com.tangem.feature.swap.viewmodels.SwapProcessDataState import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DASH_SIGN +import com.tangem.utils.StringsSigns.PERCENT import com.tangem.utils.StringsSigns.TILDE_SIGN import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -166,10 +172,15 @@ internal class StateBuilder( val canSelectReceiveToken = mainTokenId != toToken.id.value if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder - val sendInput = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable).copy( - isError = false, - header = TextReference.Res(R.string.swapping_from_title), - ) + val sendInputType = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable) + val sendInput = if (sendInputType.isError) { + sendInputType + } else { + sendInputType.copy( + isError = false, + header = TextReference.Res(R.string.swapping_from_title), + ) + } return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( type = sendInput, @@ -246,10 +257,16 @@ internal class StateBuilder( } else { TextReference.Res(R.string.swapping_from_title) } - val sendInput = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable).copy( - isError = isInsufficientFunds, - header = insufficientFundsHeader, - ) + val sendCardType = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable) + val sendInput = if (sendCardType.isError && !isInsufficientFunds) { + // if any error in inputField and funds enough -> show that error else show fund is not enough error + sendCardType + } else { + sendCardType.copy( + isError = isInsufficientFunds, + header = insufficientFundsHeader, + ) + } return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( type = sendInput, @@ -469,10 +486,8 @@ internal class StateBuilder( domainWarning: Warning.ExistentialDepositWarning, ): SwapWarning { val fromCurrency = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency - val deposit = BigDecimalFormatter.formatCryptoAmountUncapped( - cryptoAmount = domainWarning.existentialDeposit, - cryptoCurrency = fromCurrency, - ) + val deposit = domainWarning.existentialDeposit.format { crypto(fromCurrency).uncapped() } + return SwapWarning.GeneralError( NotificationConfig( title = resourceReference(R.string.send_notification_existential_deposit_title), @@ -859,14 +874,36 @@ internal class StateBuilder( ) } - fun updateSwapAmount(uiState: SwapStateHolder, amount: String): SwapStateHolder { + fun updateSwapAmount( + uiState: SwapStateHolder, + amountFormatted: String, + amountRaw: String, + fromToken: CryptoCurrency, + minTxAmount: BigDecimal?, + ): SwapStateHolder { if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState + val amountToSend = amountRaw.toBigDecimalOrNull() + val sendInput = if (minTxAmount != null && amountToSend != null && amountToSend < minTxAmount) { + val minAmountFormatted = minTxAmount.format { + crypto(cryptoCurrency = fromToken, ignoreSymbolPosition = true) + } + (uiState.sendCardData.type as? TransactionCardType.Inputtable)?.copy( + isError = true, + header = resourceReference(R.string.transfer_min_amount_error, wrappedList(minAmountFormatted)), + ) ?: uiState.sendCardData.type + } else { + (uiState.sendCardData.type as? TransactionCardType.Inputtable)?.copy( + isError = false, + header = TextReference.Res(R.string.swapping_from_title), + ) ?: uiState.sendCardData.type + } return uiState.copy( sendCardData = uiState.sendCardData.copy( amountTextFieldValue = TextFieldValue( - text = amount, - selection = TextRange(amount.length), + text = amountFormatted, + selection = TextRange(amountFormatted.length), ), + type = sendInput, ), ) } @@ -1087,28 +1124,49 @@ internal class StateBuilder( uiState: SwapStateHolder, isPriceImpact: Boolean, token: String, - providerType: ExchangeProviderType, + provider: SwapProvider, onDismiss: () -> Unit, ): SwapStateHolder { - val message = when (providerType) { - ExchangeProviderType.CEX -> resourceReference(R.string.swapping_alert_cex_description, wrappedList(token)) - ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { - val refs = buildList { + val slippage = provider.slippage?.let { "${it.parseBigDecimal(1)}$PERCENT" } + val combinedMessage = buildList { + when (provider.type) { + ExchangeProviderType.CEX -> { + if (slippage != null) { + add( + resourceReference( + id = R.string.swapping_alert_cex_description_with_slippage, + formatArgs = wrappedList(token, slippage), + ), + ) + } else { + add(resourceReference(R.string.swapping_alert_cex_description, wrappedList(token))) + } + } + ExchangeProviderType.DEX, + ExchangeProviderType.DEX_BRIDGE, + -> { if (isPriceImpact) { add(resourceReference(R.string.swapping_high_price_impact_description)) add(stringReference("\n\n")) } - add(resourceReference(R.string.swapping_alert_dex_description)) + if (slippage != null) { + add( + resourceReference( + id = R.string.swapping_alert_dex_description_with_slippage, + formatArgs = wrappedList(token, slippage), + ), + ) + } else { + add(resourceReference(R.string.swapping_alert_dex_description, wrappedList(token))) + } } - - combinedReference(refs.toWrappedList()) } } return uiState.copy( event = triggeredEvent( SwapEvent.ShowAlert( - SwapAlertUM.FeesAlert( - message = message, + SwapAlertUM.InformationAlert( + message = combinedReference(combinedMessage.toWrappedList()), onConfirmClick = onDismiss, ), ), @@ -1547,8 +1605,12 @@ internal class StateBuilder( toTokenInfo.cryptoCurrencyStatus.currency.decimals, ) val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol - val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol - val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" + val rateString = buildString { + append(BigDecimal.ONE.format { crypto(symbol = fromCurrencySymbol, decimals = 0).anyDecimals() }) + append(" ≈ ") + append(rate.format { crypto(toTokenInfo.cryptoCurrencyStatus.currency) }) + } + // val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" val badge = if (isRecommended) { ProviderState.AdditionalBadge.Recommended } else if (isNeedBestRateBadge && isBestRate) { @@ -1629,7 +1691,7 @@ internal class StateBuilder( private fun CryptoCurrencyStatus.getFormattedAmount(isNeedSymbol: Boolean): String { val amount = value.amount ?: return DASH_SIGN val symbol = if (isNeedSymbol) currency.symbol else "" - return BigDecimalFormatter.formatCryptoAmount(amount, symbol, currency.decimals) + return amount.format { crypto(symbol, currency.decimals) } } @Suppress("UnusedPrivateMember") @@ -1647,7 +1709,7 @@ internal class StateBuilder( } private fun SwapAmount.getFormattedCryptoAmount(token: CryptoCurrency): String { - return BigDecimalFormatter.formatCryptoAmount(value, token.symbol, token.decimals) + return value.format { crypto(token) } } private fun BigDecimal.calculateRate(to: BigDecimal, decimals: Int): BigDecimal { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/utils/SwapUtils.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/utils/SwapUtils.kt index e288546e37..44c918c906 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/utils/SwapUtils.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/utils/SwapUtils.kt @@ -3,7 +3,10 @@ package com.tangem.feature.swap.utils import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.simple import com.tangem.feature.swap.domain.models.ExpressDataError +import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.presentation.R internal fun getExpressErrorMessage(expressDataError: ExpressDataError): TextReference { @@ -38,4 +41,8 @@ internal fun getExpressErrorTitle(expressDataError: ExpressDataError): TextRefer is ExpressDataError.UnknownError -> resourceReference(R.string.common_error) else -> resourceReference(R.string.warning_express_refresh_required_title) } +} + +internal fun SwapAmount.formatToUIRepresentation(): String { + return value.format { simple(decimals = decimals) } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 24e23b5336..216f2860fa 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -26,6 +26,7 @@ import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -40,7 +41,6 @@ import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.ExpressException import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* -import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.SwapWarning @@ -49,6 +49,7 @@ import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder +import com.tangem.feature.swap.utils.formatToUIRepresentation import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero @@ -83,6 +84,7 @@ internal class SwapViewModel @Inject constructor( private val getCardInfoUseCase: GetCardInfoUseCase, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, swapInteractorFactory: SwapInteractor.Factory, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { @@ -842,60 +844,78 @@ internal class SwapViewModel @Inject constructor( } private fun onChangeCardsClicked() { - val newFromToken = dataState.toCryptoCurrency - val newToToken = dataState.fromCryptoCurrency + viewModelScope.launch { + val newFromToken = dataState.toCryptoCurrency + val newToToken = dataState.fromCryptoCurrency - if (newFromToken != null && newToToken != null) { - isAmountChangedByUser = true + if (newFromToken != null && newToToken != null) { + isAmountChangedByUser = true - dataState = dataState.copy( - fromCryptoCurrency = newFromToken, - toCryptoCurrency = newToToken, - ) - isOrderReversed = !isOrderReversed - dataState.tokensDataState?.let { - updateTokensState(it) + dataState = dataState.copy( + fromCryptoCurrency = newFromToken, + toCryptoCurrency = newToToken, + ) + isOrderReversed = !isOrderReversed + dataState.tokensDataState?.let { + updateTokensState(it) + } + + val minTxAmount = getMinimumTransactionAmountSyncUseCase( + userWalletId, + newFromToken, + ).getOrNull() + val decimals = newFromToken.currency.decimals + lastAmount.value = cutAmountWithDecimals(decimals, lastAmount.value) + uiState = stateBuilder.updateSwapAmount( + uiState = uiState, + amountFormatted = inputNumberFormatter.formatWithThousands(lastAmount.value, decimals), + amountRaw = lastAmount.value, + fromToken = newFromToken.currency, + minTxAmount = minTxAmount, + ) + startLoadingQuotes( + fromToken = newFromToken, + toToken = newToToken, + amount = lastAmount.value, + toProvidersList = findSwapProviders(newFromToken, newToToken), + ) } - - val decimals = newFromToken.currency.decimals - lastAmount.value = cutAmountWithDecimals(decimals, lastAmount.value) - uiState = stateBuilder.updateSwapAmount( - uiState, - inputNumberFormatter.formatWithThousands(lastAmount.value, decimals), - ) - startLoadingQuotes( - fromToken = newFromToken, - toToken = newToToken, - amount = lastAmount.value, - toProvidersList = findSwapProviders(newFromToken, newToToken), - ) } } private fun onAmountChanged(value: String) { - val fromToken = dataState.fromCryptoCurrency - val toToken = dataState.toCryptoCurrency - if (fromToken != null) { - val decimals = fromToken.currency.decimals - val cutValue = cutAmountWithDecimals(decimals, value) - lastAmount.value = cutValue - uiState = stateBuilder.updateSwapAmount( - uiState = uiState, - amount = inputNumberFormatter.formatWithThousands(cutValue, decimals), - ) + viewModelScope.launch { + val fromToken = dataState.fromCryptoCurrency + val toToken = dataState.toCryptoCurrency + if (fromToken != null) { + val decimals = fromToken.currency.decimals + val cutValue = cutAmountWithDecimals(decimals, value) + val minTxAmount = getMinimumTransactionAmountSyncUseCase( + userWalletId, + fromToken, + ).getOrNull() + lastAmount.value = cutValue + uiState = stateBuilder.updateSwapAmount( + uiState = uiState, + amountFormatted = inputNumberFormatter.formatWithThousands(cutValue, decimals), + amountRaw = lastAmount.value, + fromToken = fromToken.currency, + minTxAmount = minTxAmount, + ) - if (toToken != null) { - if (toToken.value.amount != null) { - isAmountChangedByUser = true - } + if (toToken != null) { + if (toToken.value.amount != null) { + isAmountChangedByUser = true + } - amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) { - startLoadingQuotes( - fromToken = fromToken, - toToken = toToken, - amount = lastAmount.value, - toProvidersList = findSwapProviders(fromToken, toToken), - ) + amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) { + startLoadingQuotes( + fromToken = fromToken, + toToken = toToken, + amount = lastAmount.value, + toProvidersList = findSwapProviders(fromToken, toToken), + ) + } } } } @@ -1067,7 +1087,7 @@ internal class SwapViewModel @Inject constructor( uiState = uiState, isPriceImpact = isPriceImpact, token = currencySymbol, - providerType = selectedProvider.type, + provider = selectedProvider, ) { uiState = stateBuilder.clearAlert(uiState) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt index 3d8785e062..5b11ac88dd 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/TokenDetailsFragment.kt @@ -20,7 +20,6 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsViewModel -import com.tangem.features.markets.MarketsFeatureToggles import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -36,9 +35,6 @@ internal class TokenDetailsFragment : ComposeFragment() { @Inject lateinit var tokenDetailsRouter: TokenDetailsRouter - @Inject - internal lateinit var marketsFeatureToggles: MarketsFeatureToggles - @Inject internal lateinit var coroutineDispatcherProvider: CoroutineDispatcherProvider @@ -61,27 +57,25 @@ internal class TokenDetailsFragment : ComposeFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (marketsFeatureToggles.isFeatureEnabled) { - val cryptoCurrency: CryptoCurrency = arguments - ?.getBundle(AppRoute.CurrencyDetails.CRYPTO_CURRENCY_KEY) - ?.unbundle(CryptoCurrency.serializer()) - ?: error("Token Details screen can't be opened without `CryptoCurrency`") + val cryptoCurrency: CryptoCurrency = arguments + ?.getBundle(AppRoute.CurrencyDetails.CRYPTO_CURRENCY_KEY) + ?.unbundle(CryptoCurrency.serializer()) + ?: error("Token Details screen can't be opened without `CryptoCurrency`") - val param = cryptoCurrency.toParam() ?: return + val param = cryptoCurrency.toParam() ?: return - val appContext = DefaultAppComponentContext( - componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), - messageHandler = uiDependencies.eventMessageHandler, - dispatchers = coroutineDispatcherProvider, - hiltComponentBuilder = componentBuilder, - replaceRouter = appRouter.asRouter(), - ) + val appContext = DefaultAppComponentContext( + componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), + messageHandler = uiDependencies.eventMessageHandler, + dispatchers = coroutineDispatcherProvider, + hiltComponentBuilder = componentBuilder, + replaceRouter = appRouter.asRouter(), + ) - tokenMarketBlockComponent = tokenMarketBlockComponentFactory.create( - appComponentContext = appContext, - params = param, - ) - } + tokenMarketBlockComponent = tokenMarketBlockComponentFactory.create( + appComponentContext = appContext, + params = param, + ) } private fun CryptoCurrency.toParam(): TokenMarketBlockComponent.Params? { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt index 0625d38565..4a76d6f1c9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt @@ -1,5 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.YieldBalance @@ -72,6 +74,6 @@ internal class TokenDetailsBalanceSelectStateConverter( val amount = status.value.amount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN val totalAmount = amount.getBalance(selectedBalanceType, stakingCryptoAmount) - return BigDecimalFormatter.formatCryptoAmount(totalAmount, status.currency.symbol, status.currency.decimals) + return totalAmount.format { crypto(status.currency) } } } \ 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 4ddc9c283e..e9d2706803 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 @@ -10,11 +10,10 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.* import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.staking.model.stakekit.RewardBlockType import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.error.CurrencyStatusError @@ -26,8 +25,10 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.get import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles import com.tangem.features.tokendetails.impl.R +import com.tangem.lib.crypto.BlockchainUtils.isBSC import com.tangem.lib.crypto.BlockchainUtils.isSolana import com.tangem.utils.Provider +import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.persistentListOf @@ -39,7 +40,6 @@ internal class TokenDetailsLoadedBalanceConverter( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, private val stakingEntryInfoProvider: Provider, - private val pendingBalancesProvider: Provider>, private val stakingAvailabilityProvider: Provider, private val symbol: String, private val decimals: Int, @@ -141,53 +141,51 @@ internal class TokenDetailsLoadedBalanceConverter( } private fun getYieldBalance(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM? { + return when (stakingAvailabilityProvider.invoke()) { + StakingAvailability.TemporaryUnavailable -> StakingBlockUM.TemporaryUnavailable + StakingAvailability.Unavailable -> null + is StakingAvailability.Available -> getStakingInfoBlock(status, state) + } + } + + private fun getStakingInfoBlock(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM? { val yieldBalance = status.value.yieldBalance as? YieldBalance.Data + val stakingCryptoAmount = yieldBalance?.getTotalStakingBalance() + val pendingBalances = yieldBalance?.balance?.items ?: emptyList() val stakingEntryInfo = stakingEntryInfoProvider.invoke() - val stakingAvailability = stakingAvailabilityProvider.invoke() val iconState = state.tokenInfoBlockState.iconState - val pendingBalances = pendingBalancesProvider.invoke() - val fiatRate = status.value.fiatRate return when { - stakingAvailability == StakingAvailability.TemporaryUnavailable -> { - StakingBlockUM.TemporaryUnavailable - } - stakingAvailability == StakingAvailability.Unavailable -> { - null - } - stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null && pendingBalances.isEmpty() -> { - getStakeAvailableState(stakingEntryInfo, iconState) - } - stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null && pendingBalances.isNotEmpty() -> { - val pendingBalancesCryptoAmount = pendingBalances.sumOf { it.amount } - - val stakingFiatAmount = fiatRate?.multiply(pendingBalancesCryptoAmount) - getStakedState( - status = status, - stakingCryptoAmount = pendingBalancesCryptoAmount, - stakingFiatAmount = stakingFiatAmount, - stakingRewardAmount = null, - ) + stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> { + if (pendingBalances.isEmpty()) { + getStakeAvailableState(stakingEntryInfo, iconState) + } else { + getStakedBlockWithFiatAmount(status, pendingBalances.sumOf { it.amount }, null) + } } stakingCryptoAmount.isNullOrZero() && stakingEntryInfo == null -> { null } - else -> { - val stakingRewardAmount = yieldBalance?.getRewardStakingBalance()?.let { fiatRate?.multiply(it) } - val stakingFiatAmount = stakingCryptoAmount?.let { fiatRate?.multiply(it) } - - getStakedState( - status = status, - stakingCryptoAmount = stakingCryptoAmount, - stakingFiatAmount = stakingFiatAmount, - stakingRewardAmount = stakingRewardAmount, - ) - } + else -> getStakedBlockWithFiatAmount(status, stakingCryptoAmount, yieldBalance?.getRewardStakingBalance()) } } + private fun getStakedBlockWithFiatAmount( + status: CryptoCurrencyStatus, + stakingAmount: BigDecimal?, + rewardAmount: BigDecimal?, + ): StakingBlockUM.Staked { + val fiatRate = status.value.fiatRate + return getStakedState( + status = status, + stakingCryptoAmount = stakingAmount, + stakingFiatAmount = stakingAmount?.let { fiatRate?.multiply(it) }, + stakingRewardAmount = rewardAmount?.let { fiatRate?.multiply(it) }, + ) + } + private fun getMarketPriceState( status: CryptoCurrencyStatus.Value, currencySymbol: String, @@ -215,10 +213,7 @@ internal class TokenDetailsLoadedBalanceConverter( stakingEntryInfo: StakingEntryInfo, iconState: IconState, ): StakingBlockUM.StakeAvailable { - val apr = BigDecimalFormatter.formatPercent( - percent = stakingEntryInfo.apr, - useAbsoluteValue = true, - ) + val apr = stakingEntryInfo.apr.format { percent() } return StakingBlockUM.StakeAvailable( titleText = resourceReference( id = R.string.token_details_staking_block_title, @@ -243,14 +238,15 @@ internal class TokenDetailsLoadedBalanceConverter( cryptoAmount = stakingCryptoAmount, fiatAmount = stakingFiatAmount, cryptoValue = stringReference( - BigDecimalFormatter.formatCryptoAmount(stakingCryptoAmount, symbol, decimals), + stakingCryptoAmount.format { crypto(symbol = symbol, decimals = decimals) }, ), fiatValue = stringReference( - BigDecimalFormatter.formatFiatAmount( - stakingFiatAmount, - appCurrencyProvider().code, - appCurrencyProvider().symbol, - ), + stakingFiatAmount.format { + fiat( + appCurrencyProvider().code, + appCurrencyProvider().symbol, + ) + }, ), rewardValue = getRewardText(status, stakingRewardAmount), onStakeClicked = clickIntents::onStakeBannerClick, @@ -273,22 +269,20 @@ internal class TokenDetailsLoadedBalanceConverter( } private fun formatPriceChange(status: CryptoCurrencyStatus.Value): String { - val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val priceChange = status.priceChange ?: return DASH_SIGN - return BigDecimalFormatter.formatPercent( - percent = priceChange, - useAbsoluteValue = true, - ) + return priceChange.format { percent() } } private fun formatPrice(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String { - val fiatRate = status.fiatRate ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val fiatRate = status.fiatRate ?: return DASH_SIGN - return BigDecimalFormatter.formatFiatAmountUncapped( - fiatAmount = fiatRate, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) + return fiatRate.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).uncapped() + } } private fun formatFiatAmount( @@ -297,14 +291,15 @@ internal class TokenDetailsLoadedBalanceConverter( selectedBalanceType: BalanceType, appCurrency: AppCurrency, ): String { - val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val fiatAmount = status.fiatAmount ?: return DASH_SIGN val totalAmount = fiatAmount.getBalance(selectedBalanceType, stakingFiatAmount) - return BigDecimalFormatter.formatFiatAmount( - fiatAmount = totalAmount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) + return totalAmount.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + } } private fun formatCryptoAmount( @@ -312,16 +307,16 @@ internal class TokenDetailsLoadedBalanceConverter( stakingCryptoAmount: BigDecimal?, selectedBalanceType: BalanceType, ): String { - val amount = status.value.amount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val amount = status.value.amount ?: return DASH_SIGN val totalAmount = amount.getBalance(selectedBalanceType, stakingCryptoAmount) - return BigDecimalFormatter.formatCryptoAmount(totalAmount, status.currency.symbol, status.currency.decimals) + return totalAmount.format { crypto(status.currency) } } private fun getRewardText(status: CryptoCurrencyStatus, stakingRewardAmount: BigDecimal?): TextReference { - val isSolana = isSolana(status.currency.network.id.value) + val blockchainId = status.currency.network.id.value val rewardBlockType = when { - isSolana -> RewardBlockType.RewardUnavailable + isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable stakingRewardAmount.isNullOrZero() -> RewardBlockType.NoRewards else -> RewardBlockType.Rewards } @@ -330,11 +325,12 @@ internal class TokenDetailsLoadedBalanceConverter( RewardBlockType.Rewards -> resourceReference( R.string.staking_details_rewards_to_claim, wrappedList( - BigDecimalFormatter.formatFiatAmount( - stakingRewardAmount, - appCurrencyProvider().code, - appCurrencyProvider().symbol, - ), + stakingRewardAmount.format { + fiat( + appCurrencyProvider().code, + appCurrencyProvider().symbol, + ) + }, ), ) RewardBlockType.NoRewards -> resourceReference(R.string.staking_details_no_rewards_to_claim) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index 0722f5a792..86aae8128f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -2,8 +2,10 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.shorted import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.model.warnings.HederaWarnings @@ -78,11 +80,9 @@ internal class TokenDetailsNotificationConverter( CryptoCurrencyWarning.SomeNetworksUnreachable -> NetworksUnreachable is CryptoCurrencyWarning.SomeNetworksNoAccount -> NetworksNoAccount( network = warning.amountCurrency.name, - amount = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = warning.amountToCreateAccount, - cryptoCurrency = "", - decimals = warning.amountCurrency.decimals, - ), + amount = warning.amountToCreateAccount.format { + crypto(symbol = "", decimals = warning.amountCurrency.decimals) + }, symbol = warning.amountCurrency.symbol, ) is CryptoCurrencyWarning.TopUpWithoutReserve -> TopUpWithoutReserve @@ -105,11 +105,7 @@ internal class TokenDetailsNotificationConverter( ) is HederaWarnings.AssociateWarningWithFee -> HederaAssociateWarning( currency = warning.currency, - fee = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = warning.fee, - cryptoCurrency = "", - decimals = warning.feeCurrencyDecimals, - ), + fee = warning.fee.format { crypto(symbol = "", decimals = warning.feeCurrencyDecimals) }, feeCurrencySymbol = warning.feeCurrencySymbol, onAssociateClick = clickIntents::onAssociateClick, ) @@ -127,11 +123,7 @@ internal class TokenDetailsNotificationConverter( } private fun formatMana(amount: BigDecimal): String { - return BigDecimalFormatter.formatCryptoAmountShorted( - cryptoAmount = amount, - cryptoCurrency = "", - decimals = Blockchain.Koinos.decimals(), - ) + return amount.format { crypto("", Blockchain.Koinos.decimals()).shorted() } } // workaround for networks that users have misunderstanding 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 80f8e8e1dc..08c2f45190 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 @@ -21,7 +21,6 @@ import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingIntegrationIdUseCase import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning @@ -54,7 +53,6 @@ internal class TokenDetailsStateFactory( private val stakingEntryInfoProvider: Provider, private val stakingAvailabilityProvider: Provider, private val cryptoCurrencyStatusProvider: Provider, - private val pendingBalancesProvider: Provider>, private val clickIntents: TokenDetailsClickIntents, private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, @@ -87,7 +85,6 @@ internal class TokenDetailsStateFactory( appCurrencyProvider = appCurrencyProvider, stakingEntryInfoProvider = stakingEntryInfoProvider, stakingAvailabilityProvider = stakingAvailabilityProvider, - pendingBalancesProvider = pendingBalancesProvider, symbol = symbol, decimals = decimals, clickIntents = clickIntents, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 57de8e0000..2ec87f5ed8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -3,6 +3,8 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter 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.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday @@ -82,17 +84,11 @@ internal class TokenDetailsSwapTransactionsStateConverter( activeStatus = transaction.status?.status, notification = notifications, toCryptoCurrency = toCryptoCurrency, - toCryptoAmount = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = toAmount, - cryptoCurrency = toCryptoCurrency, - ), + toCryptoAmount = toAmount.format { crypto(toCryptoCurrency) }, toFiatAmount = getFiatAmount(toFiatAmount), toCurrencyIcon = iconStateConverter.convert(toCryptoCurrency), fromCryptoCurrency = fromCryptoCurrency, - fromCryptoAmount = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = fromAmount, - cryptoCurrency = fromCryptoCurrency, - ), + fromCryptoAmount = fromAmount.format { crypto(fromCryptoCurrency) }, fromFiatAmount = getFiatAmount(fromFiatAmount), fromCurrencyIcon = iconStateConverter.convert(fromCryptoCurrency), showProviderLink = showProviderLink, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt index 0d0b59e987..360126d91d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/txhistory/TokenDetailsTxHistoryTransactionStateConverter.kt @@ -7,6 +7,8 @@ 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.format import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryItem.* @@ -16,7 +18,6 @@ import com.tangem.utils.StringsSigns.MINUS import com.tangem.utils.StringsSigns.PLUS import com.tangem.utils.converter.Converter import com.tangem.utils.toBriefAddressFormat -import com.tangem.utils.toFormattedCurrencyString internal class TokenDetailsTxHistoryTransactionStateConverter( private val symbol: String, @@ -49,13 +50,14 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( } else { when (type) { is TransactionType.Approve -> R.drawable.ic_doc_24 - is TransactionType.TronStakingTransactionType.Stake, - is TransactionType.TronStakingTransactionType.Vote, + is TransactionType.Staking.Stake, + is TransactionType.Staking.Vote, + is TransactionType.Staking.Restake, -> R.drawable.ic_transaction_history_staking_24 - is TransactionType.TronStakingTransactionType.ClaimRewards, + is TransactionType.Staking.ClaimRewards, -> R.drawable.ic_transaction_history_claim_rewards_24 - is TransactionType.TronStakingTransactionType.Unstake, - is TransactionType.TronStakingTransactionType.Withdraw, + is TransactionType.Staking.Unstake, + is TransactionType.Staking.Withdraw, -> R.drawable.ic_transaction_history_unstaking_24 is TransactionType.Operation, is TransactionType.Swap, @@ -71,11 +73,12 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( is TransactionType.Swap -> resourceReference(R.string.common_swap) is TransactionType.Transfer -> resourceReference(R.string.common_transfer) is TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) - is TransactionType.TronStakingTransactionType.Stake -> resourceReference(R.string.common_stake) - is TransactionType.TronStakingTransactionType.Unstake -> resourceReference(R.string.common_unstake) - is TransactionType.TronStakingTransactionType.Vote -> resourceReference(R.string.staking_vote) - is TransactionType.TronStakingTransactionType.ClaimRewards -> resourceReference(R.string.common_claim_rewards) - is TransactionType.TronStakingTransactionType.Withdraw -> resourceReference(R.string.staking_withdraw) + is TransactionType.Staking.Stake -> resourceReference(R.string.common_stake) + is TransactionType.Staking.Unstake -> resourceReference(R.string.common_unstake) + is TransactionType.Staking.Vote -> resourceReference(R.string.staking_vote) + is TransactionType.Staking.ClaimRewards -> resourceReference(R.string.common_claim_rewards) + is TransactionType.Staking.Withdraw -> resourceReference(R.string.staking_withdraw) + is TransactionType.Staking.Restake -> resourceReference(R.string.staking_restake) } private fun TxHistoryItem.extractSubtitle(): TextReference = @@ -118,9 +121,9 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( } private fun TxHistoryItem.getAmount(): String { - if (type is TransactionType.TronStakingTransactionType.Vote || - type == TransactionType.TronStakingTransactionType.ClaimRewards || - type == TransactionType.TronStakingTransactionType.Withdraw + if (type is TransactionType.Staking.Vote || + type == TransactionType.Staking.ClaimRewards || + type == TransactionType.Staking.Withdraw ) { return "" } @@ -129,6 +132,6 @@ internal class TokenDetailsTxHistoryTransactionStateConverter( this.amount.isZero() -> "" else -> if (isOutgoing) MINUS else PLUS } - return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals) + return prefix + amount.format { crypto(symbol = symbol, decimals = decimals) } } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index 74ab07f465..54e6b0dc2b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.constraintlayout.compose.ConstraintLayout import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.buttons.HorizontalActionChips import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons @@ -25,6 +26,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.toImmutableList +@Suppress("DestructuringDeclarationWithTooManyEntries") @Composable internal fun TokenDetailsBalanceBlock( state: TokenDetailsBalanceBlockState, @@ -36,43 +38,61 @@ internal fun TokenDetailsBalanceBlock( shape = TangemTheme.shapes.roundedCornersXMedium, color = TangemTheme.colors.background.primary, ) { - Column { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing12) - .fillMaxWidth() - .heightIn(min = TangemTheme.dimens.spacing24), - ) { - Text( - text = stringResource(id = R.string.common_balance_title), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.subtitle2, - maxLines = 1, - modifier = Modifier - .weight(1f) - .padding(top = TangemTheme.dimens.spacing12), - ) - BalanceButtons(state) - } + val spacing4 = TangemTheme.dimens.spacing4 + val spacing10 = TangemTheme.dimens.spacing10 + val spacing12 = TangemTheme.dimens.spacing12 + + ConstraintLayout( + modifier = Modifier + .fillMaxWidth(), + ) { + val (balanceTitle, toggleButtons, fiatBalance, cryptoBalance, actionChips) = createRefs() + + Text( + text = stringResource(id = R.string.common_balance_title), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + modifier = Modifier.constrainAs(balanceTitle) { + top.linkTo(anchor = parent.top, margin = spacing12) + start.linkTo(anchor = parent.start, margin = spacing12) + }, + ) + + BalanceButtons( + state = state, + modifier = Modifier.constrainAs(toggleButtons) { + top.linkTo(anchor = parent.top) + end.linkTo(anchor = parent.end, margin = spacing10) + }, + ) + FiatBalance( state = state, isBalanceHidden = isBalanceHidden, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing4) - .padding(horizontal = TangemTheme.dimens.spacing12), + modifier = Modifier.constrainAs(fiatBalance) { + top.linkTo(anchor = balanceTitle.bottom, margin = spacing4) + start.linkTo(anchor = parent.start, margin = spacing12) + }, ) + CryptoBalance( state = state, isBalanceHidden = isBalanceHidden, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing4) - .padding(horizontal = TangemTheme.dimens.spacing12), + modifier = Modifier.constrainAs(cryptoBalance) { + top.linkTo(anchor = fiatBalance.bottom, margin = spacing4) + start.linkTo(anchor = parent.start, margin = spacing12) + }, ) HorizontalActionChips( buttons = state.actionButtons.map(TokenDetailsActionButton::config).toImmutableList(), - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing12), + modifier = Modifier + .constrainAs(actionChips) { + top.linkTo(anchor = cryptoBalance.bottom, margin = spacing12) + start.linkTo(anchor = parent.start) + end.linkTo(anchor = parent.end) + bottom.linkTo(anchor = parent.bottom, margin = spacing12) + }, contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing12), ) } @@ -89,7 +109,7 @@ private fun FiatBalance( is TokenDetailsBalanceBlockState.Loading -> RectangleShimmer( modifier = modifier.size( width = TangemTheme.dimens.size102, - height = TangemTheme.dimens.size24, + height = TangemTheme.dimens.size32, ), ) is TokenDetailsBalanceBlockState.Content -> Text( @@ -136,14 +156,14 @@ private fun CryptoBalance( } @Composable -private fun BalanceButtons(state: TokenDetailsBalanceBlockState) { +private fun BalanceButtons(state: TokenDetailsBalanceBlockState, modifier: Modifier = Modifier) { if (state !is TokenDetailsBalanceBlockState.Content || !state.isBalanceSelectorEnabled) return SegmentedButtons( config = state.balanceSegmentedButtonConfig, onClick = state.onBalanceSelect, showIndication = false, - modifier = Modifier + modifier = modifier .padding(top = TangemTheme.dimens.spacing11) .width(IntrinsicSize.Min), ) { config -> @@ -159,8 +179,8 @@ private fun BalanceButtons(state: TokenDetailsBalanceBlockState) { maxLines = 1, modifier = Modifier .padding( - horizontal = TangemTheme.dimens.spacing4, - vertical = TangemTheme.dimens.spacing6, + horizontal = TangemTheme.dimens.spacing6, + vertical = TangemTheme.dimens.spacing4, ) .align(Alignment.Center), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 1b91c07eab..1427b63a24 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -32,14 +32,12 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase -import com.tangem.domain.staking.GetStakingPendingTransactionsUseCase import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase import com.tangem.domain.staking.GetStakingIntegrationIdUseCase import com.tangem.domain.staking.GetYieldUseCase import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo @@ -121,7 +119,6 @@ internal class TokenDetailsViewModel @Inject constructor( private val swapTransactionStatusStore: SwapTransactionStatusStore, private val isDemoCardUseCase: IsDemoCardUseCase, private val associateAssetUseCase: AssociateAssetUseCase, - private val getStakingPendingTransactionsUseCase: GetStakingPendingTransactionsUseCase, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventsHandler: AnalyticsEventHandler, private val vibratorHapticManager: VibratorHapticManager, @@ -151,8 +148,6 @@ internal class TokenDetailsViewModel @Inject constructor( private val warningsJobHolder = JobHolder() private val swapTxJobHolder = JobHolder() private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() - private val stakingPendingBalances: List - get() = getStakingPendingTransactionsUseCase(userWalletId).getOrElse { emptyList() } private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private var stakingEntryInfo: StakingEntryInfo? = null @@ -165,7 +160,6 @@ internal class TokenDetailsViewModel @Inject constructor( stakingEntryInfoProvider = Provider { stakingEntryInfo }, stakingAvailabilityProvider = Provider { stakingAvailability }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - pendingBalancesProvider = Provider { stakingPendingBalances }, clickIntents = this, networkHasDerivationUseCase = networkHasDerivationUseCase, getUserWalletUseCase = getUserWalletUseCase, @@ -562,7 +556,7 @@ internal class TokenDetailsViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrency.symbol)) - analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened) + analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrency.symbol)) internalUiState.value = stateFactory.getStateWithReceiveBottomSheet( currency = cryptoCurrency, @@ -723,8 +717,6 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onRefreshSwipe(isRefreshing: Boolean) { - analyticsEventsHandler.send(TokenScreenAnalyticsEvent.Refreshed(cryptoCurrency.symbol)) - internalUiState.value = stateFactory.getRefreshingState() viewModelScope.launch(dispatchers.main) { diff --git a/features/wallet-settings/api/build.gradle.kts b/features/wallet-settings/api/build.gradle.kts index 7c48ae5a59..aee60dc9be 100644 --- a/features/wallet-settings/api/build.gradle.kts +++ b/features/wallet-settings/api/build.gradle.kts @@ -16,4 +16,7 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) } \ No newline at end of file diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt new file mode 100644 index 0000000000..97d2d5800f --- /dev/null +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.features.wallet.featuretoggles + +/** + * Wallet feature toggles + * +[REDACTED_AUTHOR] + */ +interface WalletFeatureToggles { + + val isMainActionButtonsEnabled: Boolean +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureTogglesModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureTogglesModule.kt new file mode 100644 index 0000000000..aa51974608 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/di/WalletFeatureTogglesModule.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.wallet.di + +import com.tangem.feature.wallet.featuretoggles.DefaultWalletFeatureToggles +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface WalletFeatureTogglesModule { + + @Singleton + @Binds + fun bindWalletFeatureToggles(toggles: DefaultWalletFeatureToggles): WalletFeatureToggles +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt new file mode 100644 index 0000000000..cda1776fc1 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt @@ -0,0 +1,13 @@ +package com.tangem.feature.wallet.featuretoggles + +import com.tangem.core.featuretoggle.manager.FeatureTogglesManager +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles +import javax.inject.Inject + +internal class DefaultWalletFeatureToggles @Inject constructor( + private val featureTogglesManager: FeatureTogglesManager, +) : WalletFeatureToggles { + + override val isMainActionButtonsEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "MAIN_ACTION_BUTTONS_ENABLED") +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt index 74b81ff310..ba3c83a69c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/WalletFragment.kt @@ -12,7 +12,6 @@ import com.tangem.core.decompose.di.DecomposeComponent import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.screen.ComposeFragment import com.tangem.feature.wallet.presentation.router.InnerWalletRouter -import com.tangem.features.markets.MarketsFeatureToggles import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.wallet.navigation.WalletRouter import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -43,13 +42,10 @@ internal class WalletFragment : ComposeFragment() { @Inject internal lateinit var componentBuilder: DecomposeComponent.Builder - @Inject - internal lateinit var marketsFeatureToggles: MarketsFeatureToggles - @Inject internal lateinit var appRouter: AppRouter - private var marketsEntryComponent: MarketsEntryComponent? = null + private lateinit var marketsEntryComponent: MarketsEntryComponent private val _walletRouter: InnerWalletRouter get() = requireNotNull(walletRouter as? InnerWalletRouter) { @@ -59,17 +55,15 @@ internal class WalletFragment : ComposeFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (marketsFeatureToggles.isFeatureEnabled) { - val appContext = DefaultAppComponentContext( - componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), - messageHandler = uiDependencies.eventMessageHandler, - dispatchers = coroutineDispatcherProvider, - hiltComponentBuilder = componentBuilder, - replaceRouter = appRouter.asRouter(), - ) + val appContext = DefaultAppComponentContext( + componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher), + messageHandler = uiDependencies.eventMessageHandler, + dispatchers = coroutineDispatcherProvider, + hiltComponentBuilder = componentBuilder, + replaceRouter = appRouter.asRouter(), + ) - marketsEntryComponent = marketsEntryComponentFactory.create(appContext) - } + marketsEntryComponent = marketsEntryComponentFactory.create(appContext) } @Composable 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 3dbc17ce2f..a871134063 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 @@ -3,15 +3,16 @@ package com.tangem.feature.wallet.presentation.common.preview import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.persistentListOf internal object WalletScreenPreviewData { @@ -32,16 +33,16 @@ internal object WalletScreenPreviewData { private val textContentTokensState = WalletTokensListState.ContentState.Content( items = persistentListOf( - WalletTokensListState.TokensListItemState.NetworkGroupTitle( + TokensListItemUM.NetworkGroupTitle( id = 1, name = stringReference("Bitcoin"), ), - WalletTokensListState.TokensListItemState.Token(state = tokenItemState), - WalletTokensListState.TokensListItemState.NetworkGroupTitle( + TokensListItemUM.Token(state = tokenItemState), + TokensListItemUM.NetworkGroupTitle( id = 2, name = stringReference("Ethereum"), ), - WalletTokensListState.TokensListItemState.Token( + TokensListItemUM.Token( state = tokenItemState.copy( id = "2", titleState = TokenItemState.TitleState.Content(text = "Ethereum"), @@ -54,7 +55,7 @@ internal object WalletScreenPreviewData { ), ), ), - WalletTokensListState.TokensListItemState.Token( + TokensListItemUM.Token( state = TokenItemState.Unreachable( id = "3", iconState = CurrencyIconState.Locked, @@ -63,7 +64,7 @@ internal object WalletScreenPreviewData { onItemLongClick = {}, ), ), - WalletTokensListState.TokensListItemState.Token( + TokensListItemUM.Token( state = tokenItemState.copy( id = "4", titleState = TokenItemState.TitleState.Content(text = "Shiba Inu"), @@ -106,7 +107,7 @@ internal object WalletScreenPreviewData { ), imageResId = R.drawable.ill_wallet2_cards3_120_106, cardCount = 3, - balance = BigDecimalFormatter.EMPTY_BALANCE_SIGN, + balance = DASH_SIGN, onRenameClick = { _ -> }, onDeleteClick = {}, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt index 17dca9d4fe..15a4f2250e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt @@ -32,6 +32,7 @@ import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.RoundedActionButton import com.tangem.core.ui.components.token.TokenItem +import com.tangem.core.ui.components.tokenlist.internal.DraggableNetworkTitleItem import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.reordarable.ReorderableItem @@ -40,7 +41,6 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkTitleItem import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index d08f24beca..a2e325f621 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -42,7 +42,7 @@ internal class DefaultWalletRouter( override fun getEntryFragment(): Fragment = WalletFragment.create() @Composable - override fun Initialize(onFinish: () -> Unit, marketsEntryComponent: MarketsEntryComponent?) { + override fun Initialize(onFinish: () -> Unit, marketsEntryComponent: MarketsEntryComponent) { this.onFinish = onFinish NavHost( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 40ebf62fed..344887162c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -24,7 +24,7 @@ internal interface InnerWalletRouter : WalletRouter { * @param onFinish finish activity callback */ @Composable - fun Initialize(onFinish: () -> Unit, marketsEntryComponent: MarketsEntryComponent?) + fun Initialize(onFinish: () -> Unit, marketsEntryComponent: MarketsEntryComponent) /** Pop back stack */ fun popBackStack() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/PortfolioEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/PortfolioEvent.kt deleted file mode 100644 index 85553a1445..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/PortfolioEvent.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.analytics - -import com.tangem.core.analytics.models.AnalyticsEvent - -sealed class PortfolioEvent( - event: String, - params: Map = mapOf(), -) : AnalyticsEvent("Portfolio", event, params) { - - object Refreshed : PortfolioEvent("Refreshed") - - object ButtonManageTokens : PortfolioEvent("Button - Manage Tokens") - - object TokenTapped : PortfolioEvent("Token is Tapped") - - object OrganizeTokens : PortfolioEvent("Button - Organize Tokens") -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index c0021f5d7e..bfb7efaf45 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -23,8 +23,6 @@ sealed class WalletScreenAnalyticsEvent { override val oneTimeEventId: String = id + userWalletId.stringValue } - data object WalletOpened : Basic(event = "Wallet Opened") - class CardWasScanned(source: AnalyticsParam.ScreensSources) : Basic( event = "Card Was Scanned", params = mapOf( @@ -74,7 +72,6 @@ sealed class WalletScreenAnalyticsEvent { ) : AnalyticsEvent(category = "Main Screen", event = event, params = params) { data object ScreenOpened : MainScreen(event = "Screen opened") - data object WalletSwipe : MainScreen(event = "Wallet Swipe") class EnableBiometrics(state: AnalyticsParam.OnOffState) : MainScreen( event = "Enable Biometric", @@ -88,10 +85,14 @@ sealed class WalletScreenAnalyticsEvent { data object NoticeBackupYourWalletTapped : MainScreen(event = "Notice - Backup Your Wallet Tapped") data object NoticeScanYourCardTapped : MainScreen(event = "Notice - Scan Your Card Tapped") - data object NoticeWalletLocked : MainScreen(event = "Notice - Wallet Locked") data object WalletUnlockTapped : MainScreen(event = "Notice - Wallet Unlock Tapped") - data object NetworksUnreachable : MainScreen(event = "Notice - Networks Unreachable") + class NetworksUnreachable( + tokens: List, + ) : MainScreen( + event = "Notice - Networks Unreachable", + params = mapOf("Tokens" to tokens.joinToString()), + ) data object MissingAddresses : MainScreen(event = "Notice - Missing Addresses") diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index 5d9840556b..c7aefdd0f1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -9,7 +9,6 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.analytics.CheckIsWalletToppedUpUseCase import com.tangem.domain.analytics.model.WalletBalanceState import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet @@ -38,7 +37,7 @@ internal class TokenListAnalyticsSender @Inject constructor( if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return if (tokenList.totalFiatBalance is TotalFiatBalance.Loading) return - val currenciesStatuses = getCurrenciesStatuses(tokenList) + val currenciesStatuses = tokenList.flattenCurrencies() sendBalanceLoadedEventIfNeeded(tokenList.totalFiatBalance, currenciesStatuses) sendToppedUpEventIfNeeded(userWallet, tokenList.totalFiatBalance, currenciesStatuses) @@ -46,12 +45,6 @@ internal class TokenListAnalyticsSender @Inject constructor( sendTokenBalancesIfNeeded(currenciesStatuses) } - private fun getCurrenciesStatuses(tokenList: TokenList): List = when (tokenList) { - is TokenList.Empty -> emptyList() - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) - is TokenList.Ungrouped -> tokenList.currencies - } - private fun sendBalanceLoadedEventIfNeeded( fiatBalance: TotalFiatBalance, currenciesStatuses: List, @@ -173,12 +166,12 @@ internal class TokenListAnalyticsSender @Inject constructor( } private fun sendUnreachableNetworksEventIfNeeded(currenciesStatuses: List) { - val hasUnreachableCurrencies = currenciesStatuses.any { - it.value is CryptoCurrencyStatus.Unreachable - } + val unreachableCurrencies = currenciesStatuses + .filter { it.value is CryptoCurrencyStatus.Unreachable } + .map { it.currency.symbol } - if (hasUnreachableCurrencies) { - analyticsEventHandler.send(MainScreen.NetworksUnreachable) + if (unreachableCurrencies.isNotEmpty()) { + analyticsEventHandler.send(MainScreen.NetworksUnreachable(unreachableCurrencies)) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 5ea90e12d4..7178667b98 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -7,11 +7,9 @@ import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.promo.PromoBanner import com.tangem.domain.settings.IsReadyToShowRateAppUseCase import com.tangem.domain.settings.ShouldShowRingPromoUseCase -import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.repository.PromoRepository import com.tangem.domain.wallets.models.UserWallet @@ -30,7 +28,7 @@ import kotlin.collections.count @Suppress("LongParameterList") @ViewModelScoped internal class GetMultiWalletWarningsFactory @Inject constructor( - private val getTokenListUseCase: GetTokenListUseCase, + private val tokenListStore: MultiWalletTokenListStore, private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, private val shouldShowRingPromoUseCase: ShouldShowRingPromoUseCase, @@ -44,7 +42,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val promoFlow = flow { emit(promoRepository.getRingPromoBanner()) } return combine( - flow = getTokenListUseCase.launch(userWallet.walletId), + flow = tokenListStore.getOrThrow(userWallet.walletId), flow2 = isReadyToShowRateAppUseCase(), flow3 = isNeedToBackupUseCase(userWallet.walletId), flow4 = shouldShowRingPromoUseCase(userWalletId = userWallet.walletId), @@ -129,6 +127,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( clickIntents: WalletClickIntents, ) { val currencies = maybeTokenList.getMissingAddressCurrencies() + .ifEmpty { return } addIf( element = WalletNotification.Informational.MissingAddresses( @@ -144,13 +143,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun Lce.getMissingAddressCurrencies(): List { val tokenList = getOrNull(isPartialContentAccepted = false) ?: return emptyList() - val currencies = when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) - is TokenList.Ungrouped -> tokenList.currencies - is TokenList.Empty -> emptyList() - } - - return currencies + return tokenList + .flattenCurrencies() .filter { it.value is CryptoCurrencyStatus.MissedDerivation } .map(CryptoCurrencyStatus::currency) } @@ -182,13 +176,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun Lce.hasUnreachableNetworks(): Boolean { val tokenList = getOrNull(isPartialContentAccepted = false) ?: return false - val currencies = when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) - is TokenList.Ungrouped -> tokenList.currencies - is TokenList.Empty -> emptyList() - } - - return currencies.any { it.value is CryptoCurrencyStatus.Unreachable } + return tokenList.flattenCurrencies().any { it.value is CryptoCurrencyStatus.Unreachable } } private fun MutableList.addRateTheAppNotification( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt new file mode 100644 index 0000000000..d25f60ca3e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt @@ -0,0 +1,62 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.tokens.error.TokenListError +import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.UserWalletId +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.shareIn +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +@ViewModelScoped +internal class MultiWalletTokenListStore @Inject constructor( + private val getTokenListUseCase: GetTokenListUseCase, +) { + + private val flows: ConcurrentHashMap> by lazy { + ConcurrentHashMap() + } + + fun addIfNot(userWalletId: UserWalletId, coroutineScope: CoroutineScope) { + if (flows[userWalletId] != null) { + Timber.d("Flow with token list for $userWalletId already exists") + return + } + + coroutineScope.ensureActive() + + flows[userWalletId] = getTokenListUseCase + .launch(userWalletId) + .shareIn( + scope = coroutineScope, + started = SharingStarted.WhileSubscribed(), + replay = 1, + ) + + Timber.d("Flow with token list for $userWalletId created") + } + + fun getOrThrow(userWalletId: UserWalletId): LceFlow { + return requireNotNull(flows[userWalletId]) { + "Flow with token list for $userWalletId doesn't exist" + } + } + + fun remove(userWalletId: UserWalletId) { + flows.remove(userWalletId) + + Timber.d("Flow with token list for $userWalletId removed") + } + + fun clear() { + flows.clear() + + Timber.d("All flows with token list cleared") + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt index 1f191d3585..21890462f2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -61,6 +61,18 @@ internal enum class Wallet2CobrandImage( batchIds = setOf("AF28"), ), + CryptoCasey( + cards2ResId = R.drawable.ill_crypto_casey_card2_120_106, + cards3ResId = R.drawable.ill_crypto_casey_card3_120_106, + batchIds = setOf("AF21", "AF22", "AF23"), + ), + + CryptoOrg( + cards2ResId = R.drawable.ill_crypto_org_card2_120_106, + cards3ResId = R.drawable.ill_crypto_org_card3_120_106, + batchIds = setOf("AF57"), + ), + CryptoSeth( cards2ResId = R.drawable.ill_crypto_seth_card2_120_106, cards3ResId = R.drawable.ill_crypto_seth_card3_120_106, @@ -134,6 +146,12 @@ internal enum class Wallet2CobrandImage( batchIds = setOf("AF19"), ), + StealthCard( + cards2ResId = R.drawable.ill_stealth_cards2_120_106, + cards3ResId = R.drawable.ill_stealth_cards3_120_106, + batchIds = setOf("AF60", "AF74", "AF88"), + ), + Trillant( cards2ResId = R.drawable.ill_trillant_card2_120_106, cards3ResId = R.drawable.ill_trillant_card3_120_106, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt index 3de82a88d3..f258bf29b6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -4,7 +4,8 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.plus import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.wallets.models.UserWallet @@ -97,13 +98,7 @@ internal object WalletAdditionalInfoFactory { WalletAdditionalInfo(hideable = false, content = TextReference.Res(R.string.common_locked)) } else { val blockchain = scanResponse.cardTypesResolver.getBlockchain() - val amount = currencyAmount?.let { - BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = it, - cryptoCurrency = blockchain.currency, - decimals = blockchain.decimals(), - ) - } + val amount = currencyAmount?.format { crypto(blockchain.currency, blockchain.decimals()) } WalletAdditionalInfo(hideable = true, content = TextReference.Str(value = amount.orEmpty())) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt index bec1af9947..586234c6d6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletWithFundsChecker.kt @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.common.extensions.isZero import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import javax.inject.Inject @@ -12,15 +11,7 @@ internal class WalletWithFundsChecker @Inject constructor( ) { suspend fun check(tokenList: TokenList) { - val hasNonZeroWallets = when (tokenList) { - is TokenList.GroupedByNetwork -> { - tokenList.groups - .flatMap(NetworkGroup::currencies) - .hasNonZeroWallets() - } - is TokenList.Ungrouped -> tokenList.currencies.hasNonZeroWallets() - is TokenList.Empty -> false - } + val hasNonZeroWallets = tokenList.flattenCurrencies().hasNonZeroWallets() if (hasNonZeroWallets) setWalletWithFundsFoundUseCase() } 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 7dc59658c4..ee5cdac9f1 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 @@ -2,12 +2,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase -import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletTokenListSubscriber @@ -23,7 +23,7 @@ internal class MultiWalletContentLoader( private val tokenListAnalyticsSender: TokenListAnalyticsSender, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val walletWithFundsChecker: WalletWithFundsChecker, - private val getTokenListUseCase: GetTokenListUseCase, + private val tokenListStore: MultiWalletTokenListStore, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, @@ -38,7 +38,7 @@ internal class MultiWalletContentLoader( clickIntents = clickIntents, tokenListAnalyticsSender = tokenListAnalyticsSender, walletWithFundsChecker = walletWithFundsChecker, - getTokenListUseCase = getTokenListUseCase, + tokenListStore = tokenListStore, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, applyTokenListSortingUseCase = applyTokenListSortingUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, 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 1b405c8af6..7a70b865b2 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 @@ -2,12 +2,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase -import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents @@ -21,7 +21,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val tokenListAnalyticsSender: TokenListAnalyticsSender, private val walletWithFundsChecker: WalletWithFundsChecker, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val getTokenListUseCase: GetTokenListUseCase, + private val tokenListStore: MultiWalletTokenListStore, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, @@ -35,7 +35,7 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( stateHolder = stateHolder, tokenListAnalyticsSender = tokenListAnalyticsSender, walletWithFundsChecker = walletWithFundsChecker, - getTokenListUseCase = getTokenListUseCase, + tokenListStore = tokenListStore, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index f3829783da..897b44ae92 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -1,12 +1,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.tokens.GetNodlTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber @@ -23,7 +23,7 @@ internal class SingleWalletWithTokenContentLoader( private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val walletWithFundsChecker: WalletWithFundsChecker, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val getNodlTokenListUseCase: GetNodlTokenListUseCase, + private val tokenListStore: MultiWalletTokenListStore, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, ) : WalletContentLoader(id = userWallet.walletId) { @@ -36,7 +36,7 @@ internal class SingleWalletWithTokenContentLoader( clickIntents = clickIntents, tokenListAnalyticsSender = tokenListAnalyticsSender, walletWithFundsChecker = walletWithFundsChecker, - getNodlTokenListUseCase = getNodlTokenListUseCase, + tokenListStore = tokenListStore, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt index 5d820846c7..1c433dc756 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -1,12 +1,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.tokens.GetNodlTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents @@ -19,7 +19,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( private val tokenListAnalyticsSender: TokenListAnalyticsSender, private val walletWithFundsChecker: WalletWithFundsChecker, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val getNodlTokenListUseCase: GetNodlTokenListUseCase, + private val tokenListStore: MultiWalletTokenListStore, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val runPolkadotAccountHealthCheckUseCase: RunPolkadotAccountHealthCheckUseCase, @@ -33,7 +33,7 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( tokenListAnalyticsSender = tokenListAnalyticsSender, walletWithFundsChecker = walletWithFundsChecker, getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - getNodlTokenListUseCase = getNodlTokenListUseCase, + tokenListStore = tokenListStore, getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt index c582f8d8d8..8dddf4cecc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference import com.tangem.feature.wallet.impl.R import kotlinx.collections.immutable.ImmutableList @@ -14,39 +15,27 @@ internal sealed class WalletTokensListState { sealed class ContentState : WalletTokensListState() { - abstract val items: ImmutableList + abstract val items: ImmutableList abstract val organizeTokensButtonConfig: OrganizeTokensButtonConfig? data object Loading : ContentState() { - override val items = persistentListOf() + override val items = persistentListOf() override val organizeTokensButtonConfig = null } data class Content( - override val items: ImmutableList, + override val items: ImmutableList, override val organizeTokensButtonConfig: OrganizeTokensButtonConfig?, ) : ContentState() data object Locked : ContentState() { override val items = persistentListOf( - TokensListItemState.NetworkGroupTitle(id = 42, name = TextReference.Res(id = R.string.main_tokens)), - TokensListItemState.Token(state = TokenItemState.Locked(id = "Locked#1")), + TokensListItemUM.NetworkGroupTitle(id = 42, name = TextReference.Res(id = R.string.main_tokens)), + TokensListItemUM.Token(state = TokenItemState.Locked(id = "Locked#1")), ) override val organizeTokensButtonConfig = null } } data class OrganizeTokensButtonConfig(val isEnabled: Boolean, val onClick: () -> Unit) - - @Immutable - sealed class TokensListItemState { - - abstract val id: Any - - data class NetworkGroupTitle(override val id: Int, val name: TextReference) : TokensListItemState() - - data class Token(val state: TokenItemState) : TokensListItemState() { - override val id: String = state.id - } - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt index d4f7d3db34..af365a24d7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt @@ -3,6 +3,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import arrow.core.Either import arrow.core.getOrElse import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.visa.model.VisaCurrency @@ -41,11 +43,9 @@ internal class SetBalancesAndLimitsTransformer( } private fun getContentBlockState(visaCurrency: VisaCurrency) = BalancesAndLimitsBlockState.Content( - availableBalance = BigDecimalFormatter.formatCryptoAmount( - visaCurrency.limits.remainingOtp, - visaCurrency.symbol, - visaCurrency.decimals, - ), + availableBalance = visaCurrency.limits.remainingOtp.format { + crypto(visaCurrency.symbol, visaCurrency.decimals) + }, limitDays = Days.daysBetween(DateTime.now(), visaCurrency.limits.expirationDate).days.inc(), isEnabled = true, onClick = clickIntents::onBalancesAndLimitsClick, @@ -72,11 +72,9 @@ internal class SetBalancesAndLimitsTransformer( imageResId = imageResId, onRenameClick = onRenameClick, onDeleteClick = onDeleteClick, - balance = BigDecimalFormatter.formatCryptoAmount( - visaCurrency.balances.available, - visaCurrency.symbol, - visaCurrency.decimals, - ), + balance = visaCurrency.balances.available.format { + crypto(visaCurrency.symbol, visaCurrency.decimals) + }, cardCount = userWallet.getCardsCount(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt index 146e196a65..dc965b29e2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/BalancesAndLimitsBottomSheetConverter.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.visa.model.VisaCurrency import com.tangem.feature.wallet.presentation.wallet.state.model.BalancesAndLimitsBottomSheetConfig @@ -15,11 +16,7 @@ internal class BalancesAndLimitsBottomSheetConverter( ) : Converter { override fun convert(value: VisaCurrency): BalancesAndLimitsBottomSheetConfig { - fun formatAmount(amount: BigDecimal): String = BigDecimalFormatter.formatCryptoAmount( - amount, - cryptoCurrency = value.symbol, - decimals = value.decimals, - ) + fun formatAmount(amount: BigDecimal): String = amount.format { crypto(value.symbol, value.decimals) } val otpLimit = value.limits.remainingOtp.let(::formatAmount) val noOtpLimit = value.limits.remainingNoOtp.let(::formatAmount) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt index 8d710656dc..3611aa8119 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletMarketPriceConverter.kt @@ -4,6 +4,8 @@ import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -57,7 +59,7 @@ internal class SingleWalletMarketPriceConverter( private fun formatPriceChange(status: CryptoCurrencyStatus.Value): String { val priceChange = status.priceChange ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN - return BigDecimalFormatter.formatPercent(percent = priceChange, useAbsoluteValue = true) + return priceChange.format { percent() } } private fun getPriceChangeType(status: CryptoCurrencyStatus.Value): PriceChangeType { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 4ae09b322f..668118d2fb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.util.cardTypesResolver @@ -10,7 +11,6 @@ import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.PersistentList @@ -47,20 +47,20 @@ internal class TokenListStateConverter( } } - private fun TokenList.GroupedByNetwork.toGroupedItems(): PersistentList { + private fun TokenList.GroupedByNetwork.toGroupedItems(): PersistentList { return groups.fold(initial = persistentListOf()) { acc, group -> acc.mutate { it.addGroup(group) } } } - private fun TokenList.Ungrouped.toUngroupedItems(): PersistentList { + private fun TokenList.Ungrouped.toUngroupedItems(): PersistentList { return currencies.fold(initial = persistentListOf()) { acc, token -> acc.mutate { it.addToken(token) } } } - private fun MutableList.addGroup(group: NetworkGroup): List { - val groupTitle = TokensListItemState.NetworkGroupTitle( + private fun MutableList.addGroup(group: NetworkGroup): List { + val groupTitle = TokensListItemUM.NetworkGroupTitle( id = group.network.hashCode(), name = stringReference(group.network.name), ) @@ -71,9 +71,9 @@ internal class TokenListStateConverter( return this } - private fun MutableList.addToken(token: CryptoCurrencyStatus): List { + private fun MutableList.addToken(token: CryptoCurrencyStatus): List { val tokenItemState = tokenStatusConverter.convert(token) - add(TokensListItemState.Token(tokenItemState)) + add(TokensListItemUM.Token(tokenItemState)) return this } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt index 2d162a17c0..a85ca99eab 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TxHistoryItemStateConverter.kt @@ -6,6 +6,8 @@ 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.format import com.tangem.core.ui.utils.toTimeFormat import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.txhistory.models.TxHistoryItem.* @@ -15,7 +17,6 @@ import com.tangem.utils.StringsSigns.MINUS import com.tangem.utils.StringsSigns.PLUS import com.tangem.utils.converter.Converter import com.tangem.utils.toBriefAddressFormat -import com.tangem.utils.toFormattedCurrencyString internal class TxHistoryItemStateConverter( private val symbol: String, @@ -48,13 +49,14 @@ internal class TxHistoryItemStateConverter( } else { when (type) { is TransactionType.Approve -> R.drawable.ic_doc_24 - is TransactionType.TronStakingTransactionType.Stake, - is TransactionType.TronStakingTransactionType.Vote, + is TransactionType.Staking.Stake, + is TransactionType.Staking.Vote, + is TransactionType.Staking.Restake, -> R.drawable.ic_transaction_history_staking_24 - is TransactionType.TronStakingTransactionType.ClaimRewards, + is TransactionType.Staking.ClaimRewards, -> R.drawable.ic_transaction_history_claim_rewards_24 - is TransactionType.TronStakingTransactionType.Unstake, - is TransactionType.TronStakingTransactionType.Withdraw, + is TransactionType.Staking.Unstake, + is TransactionType.Staking.Withdraw, -> R.drawable.ic_transaction_history_unstaking_24 is TransactionType.Operation, is TransactionType.Swap, @@ -69,11 +71,12 @@ internal class TxHistoryItemStateConverter( is TransactionType.Operation -> stringReference(type.name) is TransactionType.Swap -> resourceReference(R.string.common_swap) is TransactionType.Transfer -> resourceReference(R.string.common_transfer) - is TransactionType.TronStakingTransactionType.Stake -> resourceReference(R.string.common_stake) - is TransactionType.TronStakingTransactionType.Unstake -> resourceReference(R.string.common_unstake) - is TransactionType.TronStakingTransactionType.Vote -> resourceReference(R.string.staking_vote) - is TransactionType.TronStakingTransactionType.ClaimRewards -> resourceReference(R.string.common_claim_rewards) - is TransactionType.TronStakingTransactionType.Withdraw -> { resourceReference(R.string.staking_withdraw) } + is TransactionType.Staking.Stake -> resourceReference(R.string.common_stake) + is TransactionType.Staking.Unstake -> resourceReference(R.string.common_unstake) + is TransactionType.Staking.Vote -> resourceReference(R.string.staking_vote) + is TransactionType.Staking.ClaimRewards -> resourceReference(R.string.common_claim_rewards) + is TransactionType.Staking.Withdraw -> resourceReference(R.string.staking_withdraw) + is TransactionType.Staking.Restake -> resourceReference(R.string.staking_restake) is TransactionType.UnknownOperation -> resourceReference(R.string.transaction_history_operation) } @@ -118,9 +121,9 @@ internal class TxHistoryItemStateConverter( } private fun TxHistoryItem.getAmount(): String { - if (type is TransactionType.TronStakingTransactionType.Vote || - type == TransactionType.TronStakingTransactionType.ClaimRewards || - type == TransactionType.TronStakingTransactionType.Withdraw + if (type is TransactionType.Staking.Vote || + type == TransactionType.Staking.ClaimRewards || + type == TransactionType.Staking.Withdraw ) { return "" } @@ -129,6 +132,6 @@ internal class TxHistoryItemStateConverter( this.amount.isZero() -> "" else -> if (isOutgoing) MINUS else PLUS } - return prefix + amount.toFormattedCurrencyString(currency = symbol, decimals = decimals) + return prefix + amount.format { crypto(symbol = symbol, decimals = decimals) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt index 6e8158da13..e655973c16 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxDetailsBottomSheetConverter.kt @@ -1,6 +1,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter import com.tangem.core.ui.extensions.capitalize +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.visa.model.VisaCurrency @@ -66,11 +68,7 @@ internal class VisaTxDetailsBottomSheetConverter( } private fun formatNetworkAmount(amount: BigDecimal): String { - return BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = amount, - cryptoCurrency = visaCurrency.symbol, - decimals = visaCurrency.decimals, - ) + return amount.format { crypto(visaCurrency.symbol, visaCurrency.decimals) } } private fun formatFiatAmount(amount: BigDecimal, fiatCurrency: Currency): String { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt index 60ac94068a..fe50fdf537 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/VisaTxHistoryItemStateConverter.kt @@ -3,6 +3,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.components.transactions.state.TransactionState import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.visa.model.VisaCurrency @@ -25,11 +27,7 @@ internal class VisaTxHistoryItemStateConverter( return TransactionState.Content( txHash = value.id, - amount = BigDecimalFormatter.formatCryptoAmount( - cryptoAmount = value.amount, - cryptoCurrency = visaCurrency.symbol, - decimals = visaCurrency.decimals, - ), + amount = value.amount.format { crypto(visaCurrency.symbol, visaCurrency.decimals) }, // Show tx fiat amount instead of tx time time = BigDecimalFormatter.formatFiatAmount( fiatAmount = value.fiatAmount, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index f074661604..a21e20afc9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -8,7 +8,6 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.model.NetworkGroup import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender @@ -41,11 +40,11 @@ internal abstract class BasicTokenListSubscriber( private val sendAnalyticsJobHolder = JobHolder() private val onTokenListReceivedJobHolder = JobHolder() - protected abstract fun tokenListFlow(): LceFlow + protected abstract fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow override fun create(coroutineScope: CoroutineScope): Flow<*> { return combine( - flow = tokenListFlow() + flow = tokenListFlow(coroutineScope) .onEach { maybeTokenList -> coroutineScope.launch { sendTokenListAnalytics(maybeTokenList) @@ -99,15 +98,14 @@ internal abstract class BasicTokenListSubscriber( private suspend fun startCheck(maybeTokenList: Lce) { // Run Polkadot account health check maybeTokenList.getOrNull()?.let { tokenList -> - val cryptoCurrencies = when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap(NetworkGroup::currencies) - is TokenList.Ungrouped -> tokenList.currencies - is TokenList.Empty -> emptyList() - } - - cryptoCurrencies.forEach { - runPolkadotAccountHealthCheckUseCase(userWallet.walletId, it.currency.network) - } + tokenList + .flattenCurrencies() + .forEach { + runPolkadotAccountHealthCheckUseCase( + userWalletId = userWallet.walletId, + network = it.currency.network, + ) + } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt index 12d5115bcf..a1fb33dfd0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt @@ -4,7 +4,6 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.tokens.ApplyTokenListSortingUseCase -import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency @@ -12,14 +11,16 @@ import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import kotlinx.coroutines.CoroutineScope @Suppress("LongParameterList") internal class MultiWalletTokenListSubscriber( private val userWallet: UserWallet, - private val getTokenListUseCase: GetTokenListUseCase, + private val tokenListStore: MultiWalletTokenListStore, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, stateHolder: WalletStateController, clickIntents: WalletClickIntents, @@ -37,8 +38,10 @@ internal class MultiWalletTokenListSubscriber( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ) { - override fun tokenListFlow(): LceFlow { - return getTokenListUseCase.launch(userWallet.walletId) + override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { + tokenListStore.addIfNot(userWallet.walletId, coroutineScope) + + return tokenListStore.getOrThrow(userWallet.walletId) } override suspend fun onTokenListReceived(maybeTokenList: Lce) { @@ -67,12 +70,6 @@ internal class MultiWalletTokenListSubscriber( } private fun getCurrenciesIds(tokenList: TokenList): List { - return when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { group -> - group.currencies.map { it.currency.id } - } - is TokenList.Ungrouped -> tokenList.currencies.map { it.currency.id } - is TokenList.Empty -> emptyList() - } + return tokenList.flattenCurrencies().map { it.currency.id } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt index c2c561afd8..2d37fea506 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt @@ -2,22 +2,21 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.utils.toLce -import com.tangem.domain.tokens.GetNodlTokenListUseCase import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.TokenList import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.CoroutineScope @Suppress("LongParameterList") internal class SingleWalletWithTokenListSubscriber( private val userWallet: UserWallet, - private val getNodlTokenListUseCase: GetNodlTokenListUseCase, + private val tokenListStore: MultiWalletTokenListStore, stateHolder: WalletStateController, clickIntents: WalletClickIntents, tokenListAnalyticsSender: TokenListAnalyticsSender, @@ -34,6 +33,9 @@ internal class SingleWalletWithTokenListSubscriber( runPolkadotAccountHealthCheckUseCase = runPolkadotAccountHealthCheckUseCase, ) { - override fun tokenListFlow(): LceFlow = getNodlTokenListUseCase(userWallet.walletId) - .map { it.toLce() } + override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { + tokenListStore.addIfNot(userWallet.walletId, coroutineScope) + + return tokenListStore.getOrThrow(userWallet.walletId) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 4e5b6f25fd..c1d3d9cd26 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 @@ -45,7 +45,6 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.paging.compose.collectAsLazyPagingItems import com.google.accompanist.systemuicontroller.rememberSystemUiController -import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig @@ -65,7 +64,6 @@ import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.TestTags -import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.core.ui.utils.lineTo import com.tangem.core.ui.utils.moveTo import com.tangem.core.ui.utils.toPx @@ -93,7 +91,7 @@ import kotlinx.coroutines.launch import kotlin.math.roundToInt @Composable -internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: MarketsEntryComponent?) { +internal fun WalletScreen(state: WalletScreenState, marketsEntryComponent: MarketsEntryComponent) { BackHandler(onBack = state.onBackClick) // It means that screen is still initializing @@ -137,7 +135,7 @@ private fun WalletContent( walletsListState: LazyListState, snackbarHostState: SnackbarHostState, isAutoScroll: State, - marketsEntryComponent: MarketsEntryComponent?, + marketsEntryComponent: MarketsEntryComponent, alertConfig: WalletAlertState?, onAutoScrollReset: () -> Unit, ) { @@ -181,7 +179,11 @@ private fun WalletContent( contentPadding = contentPadding, horizontalAlignment = Alignment.CenterHorizontally, ) { - item(key = "WalletsList" + state.selectedWalletIndex, contentType = "WalletsList") { + item( + // !!! Type of the key should be saveable via Bundle on Android !!! + key = state.wallets.map { it.walletCardState.id.stringValue }, + contentType = state.wallets.map { it.walletCardState.id }, + ) { WalletsList( lazyListState = walletsListState, wallets = state.wallets.map(WalletState::walletCardState).toImmutableList(), @@ -239,82 +241,26 @@ private fun WalletContent( ) } - if (marketsEntryComponent != null) { - val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) } + val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) } - var headerSize by remember { mutableStateOf(0.dp) } + var headerSize by remember { mutableStateOf(0.dp) } - BaseScaffoldWithMarkets( - state = state, - listState = listState, - selectedWallet = selectedWallet, - snackbarHostState = snackbarHostState, - bottomSheetHeaderHeightProvider = { headerSize }, - alertConfig = alertConfig, - onBottomSheetStateChange = { bottomSheetState.value = it }, - bottomSheetContent = { - marketsEntryComponent.BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = { headerSize = it }, - modifier = Modifier, - ) - }, - content = scaffoldContent, - ) - } else { - BaseScaffold( - state = state, - selectedWallet = selectedWallet, - snackbarHostState = snackbarHostState, - content = { scaffoldContent(null) }, - ) - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun BaseScaffold( - state: WalletScreenState, - selectedWallet: WalletState, - snackbarHostState: SnackbarHostState, - content: @Composable () -> Unit, -) { - Scaffold( - topBar = { WalletTopBar(config = state.topBarConfig) }, - contentWindowInsets = WindowInsetsZero, - snackbarHost = { - WalletSnackbarHost( - snackbarHostState = snackbarHostState, - event = state.event, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing16), + BaseScaffoldWithMarkets( + state = state, + listState = listState, + selectedWallet = selectedWallet, + snackbarHostState = snackbarHostState, + bottomSheetHeaderHeightProvider = { headerSize }, + alertConfig = alertConfig, + onBottomSheetStateChange = { bottomSheetState.value = it }, + bottomSheetContent = { + marketsEntryComponent.BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = { headerSize = it }, + modifier = Modifier, ) }, - floatingActionButtonPosition = FabPosition.Center, - containerColor = TangemTheme.colors.background.secondary, - content = { - val pullRefreshState = rememberPullRefreshState( - refreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - onRefresh = { - selectedWallet.pullToRefreshConfig.onRefresh(PullToRefreshConfig.ShowRefreshState()) - }, - ) - - Box( - modifier = Modifier - .pullRefresh(pullRefreshState) - .padding(it), - ) { - content() - - WalletPullToRefreshIndicator( - isRefreshing = selectedWallet.pullToRefreshConfig.isRefreshing, - state = pullRefreshState, - modifier = Modifier.align(Alignment.TopCenter), - ) - - BottomFade(Modifier.align(Alignment.BottomCenter)) - } - }, + content = scaffoldContent, ) } @@ -387,7 +333,6 @@ private inline fun BaseScaffoldWithMarkets( sheetContainerColor = backgroundColor.value, scaffoldState = scaffoldState, sheetPeekHeight = peekHeight, - sheetShadowElevation = 8.dp, sheetShape = TangemTheme.shapes.bottomSheetLarge, sheetContent = { // hide bottom sheet when back pressed @@ -772,7 +717,16 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider:: TangemThemePreview { WalletScreen( state = data, - marketsEntryComponent = null, + marketsEntryComponent = object : MarketsEntryComponent { + @Composable + override fun BottomSheetContent( + bottomSheetState: State, + onHeaderSizeChange: (Dp) -> Unit, + modifier: Modifier, + ) { + Text("Markets Content") + } + }, ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index e0563a4400..49c11b5799 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -1,6 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -14,11 +13,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.components.tokenlist.TokenListItem +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState import kotlinx.collections.immutable.ImmutableList private const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST" @@ -49,7 +49,7 @@ internal fun LazyListScope.tokensListItems( } private fun LazyListScope.contentItems( - items: ImmutableList, + items: ImmutableList, modifier: Modifier = Modifier, isBalanceHidden: Boolean, ) { @@ -58,19 +58,19 @@ private fun LazyListScope.contentItems( key = { _, item -> item.id }, contentType = { _, item -> item::class.java }, itemContent = { index, item -> - MultiCurrencyContentItem( + TokenListItem( state = item, isBalanceHidden = isBalanceHidden, modifier = modifier.roundedShapeItemDecoration( currentIndex = index, lastIndex = items.lastIndex, + backgroundColor = TangemTheme.colors.background.primary, ), ) }, ) } -@OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) { item( key = NON_CONTENT_TOKENS_LIST_KEY, @@ -78,7 +78,7 @@ private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) { ) { Column( modifier = modifier - .animateItemPlacement() + .animateItem() .padding(top = TangemTheme.dimens.spacing96), verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), horizontalAlignment = Alignment.CenterHorizontally, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt deleted file mode 100644 index 6c3ca94c12..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContentItem.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency - -import androidx.compose.foundation.background -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.token.TokenItem -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.wallet.presentation.common.component.NetworkTitleItem -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.TokensListItemState - -/** - * Multi-currency content item - * - * @param state item state - * @param modifier modifier - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun MultiCurrencyContentItem( - state: TokensListItemState, - isBalanceHidden: Boolean, - modifier: Modifier = Modifier, -) { - val modifierWithBackground = modifier.background(color = TangemTheme.colors.background.primary) - - when (state) { - is TokensListItemState.NetworkGroupTitle -> { - NetworkTitleItem(networkName = state.name.resolveReference(), modifier = modifierWithBackground) - } - is TokensListItemState.Token -> { - TokenItem( - state = state.state, - isBalanceHidden = isBalanceHidden, - modifier = modifierWithBackground, - ) - } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 87496adf65..fc787ace14 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -16,6 +16,7 @@ import com.tangem.feature.wallet.presentation.deeplink.WalletDeepLinksHandler import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWalletAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader @@ -28,7 +29,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents -import com.tangem.features.markets.MarketsFeatureToggles import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull import com.tangem.utils.Provider @@ -67,8 +67,8 @@ internal class WalletViewModel @Inject constructor( private val walletNameMigrationUseCase: WalletNameMigrationUseCase, private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase, private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, - private val marketsFeatureToggles: MarketsFeatureToggles, private val walletImageResolver: WalletImageResolver, + private val tokenListStore: MultiWalletTokenListStore, analyticsEventsHandler: AnalyticsEventHandler, ) : ViewModel() { @@ -110,6 +110,8 @@ internal class WalletViewModel @Inject constructor( override fun onCleared() { super.onCleared() + + tokenListStore.clear() stateHolder.clear() walletScreenContentLoader.cancelAll() } @@ -126,7 +128,7 @@ internal class WalletViewModel @Inject constructor( viewModelScope.launch { withContext(dispatchers.io) { delay(timeMillis = 1_800) } - if (marketsFeatureToggles.isFeatureEnabled && shouldShowMarketsTooltipUseCase()) { + if (shouldShowMarketsTooltipUseCase()) { stateHolder.update { it.copy(showMarketsOnboarding = true) } @@ -280,7 +282,7 @@ internal class WalletViewModel @Inject constructor( stateHolder.update(transformer = RenameWalletTransformer(action.selectedWalletId, action.name)) } is WalletsUpdateActionResolver.Action.Unknown -> { - Timber.w("Unable to perfom action: $action") + Timber.w("Unable to perform action: $action") } } } @@ -318,6 +320,7 @@ internal class WalletViewModel @Inject constructor( private fun reinitializeWallet(action: WalletsUpdateActionResolver.Action.ReinitializeWallet) { walletScreenContentLoader.cancel(action.prevWalletId) + tokenListStore.remove(action.prevWalletId) walletScreenContentLoader.load( userWallet = action.selectedWallet, @@ -357,6 +360,7 @@ internal class WalletViewModel @Inject constructor( private suspend fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) { walletScreenContentLoader.cancel(action.deletedWalletId) + tokenListStore.remove(action.deletedWalletId) walletScreenContentLoader.load( userWallet = action.selectedWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt index 835a480b48..324b2fc7ca 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletsUpdateActionResolver.kt @@ -136,7 +136,9 @@ internal class WalletsUpdateActionResolver @Inject constructor( unlockedWallets = wallets.filterNot(UserWallet::isLocked), ) } - isSelectedWalletCardsCountChanged(state, selectedWallet) -> Action.UpdateWalletCardCount(selectedWallet) + isSelectedWalletCardsCountChanged(state, selectedWallet) -> { + Action.UpdateWalletCardCount(selectedWallet) + } else -> Action.Unknown } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt index 7c128ca00d..066fe95381 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCardClickIntents.kt @@ -12,6 +12,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.* import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen +import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState @@ -37,6 +38,7 @@ internal interface WalletCardClickIntents { @Suppress("LongParameterList") internal class WalletCardClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateController, + private val tokenListStore: MultiWalletTokenListStore, private val walletEventSender: WalletEventSender, private val walletScreenContentLoader: WalletScreenContentLoader, private val renameWalletUseCase: RenameWalletUseCase, @@ -99,6 +101,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) { viewModelScope.launch(dispatchers.main) { walletScreenContentLoader.cancel(userWalletId) + tokenListStore.remove(userWalletId) val walletToDelete = getUserWalletUseCase(userWalletId).getOrNull() ?: return@launch val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse { @@ -117,6 +120,7 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( reduxStateHolder.onUserWalletSelected(selectedWallet) } else { + tokenListStore.clear() stateHolder.clear() appRouter.replaceAll(AppRoute.Home) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt index ec3f2d992d..0c716472a9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt @@ -1,6 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents -import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.common.util.cardTypesResolver @@ -8,10 +7,10 @@ import com.tangem.domain.settings.NeverToShowWalletsScrollPreview import com.tangem.domain.tokens.FetchCardTokenListUseCase import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.FetchTokenListUseCase +import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter -import com.tangem.feature.wallet.presentation.wallet.analytics.PortfolioEvent import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -42,7 +41,6 @@ internal class WalletClickIntents @Inject constructor( private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val neverToShowWalletsScrollPreview: NeverToShowWalletsScrollPreview, - private val analyticsEventHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, ) : BaseWalletClickIntents(), WalletCardClickIntents by walletCardClickIntentsImplementor, @@ -86,13 +84,11 @@ internal class WalletClickIntents @Inject constructor( fun onRefreshSwipe(showRefreshState: Boolean) { when (stateHolder.getSelectedWallet()) { is WalletState.MultiCurrency.Content -> { - analyticsEventHandler.send(PortfolioEvent.Refreshed) refreshMultiCurrencyContent(showRefreshState) } is WalletState.SingleCurrency.Content, is WalletState.Visa.Content, -> { - analyticsEventHandler.send(PortfolioEvent.Refreshed) refreshSingleCurrencyContent(showRefreshState) } is WalletState.MultiCurrency.Locked, @@ -117,7 +113,7 @@ internal class WalletClickIntents @Inject constructor( val maybeFetchResult = if (userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()) { fetchCardTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) } else { - fetchTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) + fetchTokenListUseCase(userWalletId = userWallet.walletId, mode = RefreshMode.FULL) } maybeFetchResult.onLeft { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt index ba469eaf52..22c9441399 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -1,7 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import arrow.core.getOrElse -import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase @@ -12,7 +11,6 @@ import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.feature.wallet.presentation.wallet.analytics.PortfolioEvent import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig @@ -56,7 +54,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val shouldShowMarketsTooltipUseCase: ShouldShowMarketsTooltipUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, private val reduxStateHolder: ReduxStateHolder, ) : BaseWalletClickIntents(), WalletContentClickIntents { @@ -92,13 +89,11 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } override fun onManageTokensClick() { - analyticsEventHandler.send(PortfolioEvent.ButtonManageTokens) reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess) router.openManageTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) } override fun onOrganizeTokensClick() { - analyticsEventHandler.send(PortfolioEvent.OrganizeTokens) router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) } @@ -110,7 +105,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } override fun onTokenItemClick(currencyStatus: CryptoCurrencyStatus) { - analyticsEventHandler.send(PortfolioEvent.TokenTapped) router.openTokenDetails(stateHolder.getSelectedWalletId(), currencyStatus) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 3b57403e42..21fcf81bf1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -185,7 +185,9 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( event = TokenScreenAnalyticsEvent.ButtonReceive(cryptoCurrencyStatus.currency.symbol), ) - analyticsEventHandler.send(event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened) + analyticsEventHandler.send( + event = TokenReceiveAnalyticsEvent.ReceiveScreenOpened(cryptoCurrencyStatus.currency.symbol), + ) stateHolder.showBottomSheet( createReceiveBottomSheetContent( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index e7386f4925..2db549db07 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -13,6 +13,7 @@ import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.* import com.tangem.domain.tokens.FetchTokenListUseCase +import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType @@ -124,18 +125,20 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( analyticsEventHandler.send(Basic.CardWasScanned(AnalyticsParam.ScreensSources.Main)) analyticsEventHandler.send(MainScreen.NoticeScanYourCardTapped) - viewModelScope.launch(dispatchers.main) { + viewModelScope.launch { val userWallet = getSelectedUserWallet() ?: return@launch derivePublicKeysUseCase( userWalletId = userWallet.walletId, currencies = missedAddressCurrencies, + ).fold( + ifLeft = { Timber.e(it, "Failed to derive public keys") }, + ifRight = { + fetchTokenListUseCase(userWallet.walletId, mode = RefreshMode.SKIP_CURRENCIES).onLeft { + Timber.e("Unable to refresh token list: $it") + } + }, ) - .onRight { - // Refresh must be set to true to ensure that yield balances are updated - fetchTokenListUseCase(userWalletId = userWallet.walletId, refresh = true) - } - .onLeft { Timber.e("Failed to derive public keys: $it") } } } diff --git a/features/wallet/impl/src/main/res/drawable/ill_crypto_casey_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_crypto_casey_card2_120_106.webp new file mode 100644 index 0000000000..5aaa6f9477 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_crypto_casey_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_crypto_casey_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_crypto_casey_card3_120_106.webp new file mode 100644 index 0000000000..f749a0e5a1 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_crypto_casey_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_crypto_org_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_crypto_org_card2_120_106.webp new file mode 100644 index 0000000000..3c042caa95 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_crypto_org_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_crypto_org_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_crypto_org_card3_120_106.webp new file mode 100644 index 0000000000..ec14a4dcb5 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_crypto_org_card3_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_stealth_cards2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_stealth_cards2_120_106.webp new file mode 100644 index 0000000000..5a39ebf1ab Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_stealth_cards2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_stealth_cards3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_stealth_cards3_120_106.webp new file mode 100644 index 0000000000..0e6b50a2fe Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_stealth_cards3_120_106.webp differ diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 5322aef4fa..cb7a4dbffc 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -69,8 +69,8 @@ zxingQrCode = "3.5.1" mviCore = "1.3.1" kotlinSerialization = "1.4.1" arrow = "1.2.3" -walletConnectCore = "1.18.0" -walletConnectWeb3 = "1.11.0" +walletConnectCore = "1.35.2" +walletConnectWeb3 = "1.35.2" prettyLogger = "2.2.0" okHttp-prettyLogging = "3.1.0" chucker = "4.0.0" @@ -88,9 +88,9 @@ markdownComposeView = "0.5.4" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.17-858" +tangemBlockchainSdk = "release-app_5.18-859" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.17-392" +tangemCardSdk = "release-app_5.18-398" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem17" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt index 23379f7e5b..741f900b6d 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt @@ -39,10 +39,6 @@ internal class DefaultBlockchainSDKFactory( private val mainScope = CoroutineScope(dispatchers.main) private val walletManagerFactory: Flow = createWalletManagerFactory() - // TODO: [REDACTED_JIRA] - // private val walletManagerFactory: Flow by lazy(LazyThreadSafetyMode.NONE) { - // createWalletManagerFactory() - // } override suspend fun init() { coroutineScope { @@ -59,6 +55,7 @@ internal class DefaultBlockchainSDKFactory( // flow3 = subscribe on feature toggles changes, TODO: [REDACTED_JIRA] transform = walletManagerFactoryCreator::create, ) + // don't use Lazily because some features (WC) require initialized factory on app started .stateIn(scope = mainScope, started = SharingStarted.Eagerly, initialValue = null) } diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index e22069fd64..41bc95a6f8 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -136,6 +136,10 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "add_later_test" -> Blockchain.CasperTestnet "core" -> Blockchain.Core "core/test" -> Blockchain.CoreTestnet + "casper-network" -> Blockchain.Casper + "casper-network/test" -> Blockchain.CasperTestnet + "xodex" -> Blockchain.Xodex + "canxium" -> Blockchain.Canxium else -> null } } @@ -269,10 +273,12 @@ fun Blockchain.toNetworkId(): String { Blockchain.EnergyWebChainTestnet -> "energy-web-chain/test" Blockchain.EnergyWebX -> "energy-web-x" Blockchain.EnergyWebXTestnet -> "energy-web-x/test" - Blockchain.Casper -> "add_later" - Blockchain.CasperTestnet -> "add_later_test" + Blockchain.Casper -> "casper-network" + Blockchain.CasperTestnet -> "casper-network/test" Blockchain.Core -> "core" Blockchain.CoreTestnet -> "core/test" + Blockchain.Xodex -> "xodex" + Blockchain.Canxium -> "canxium" } } @@ -289,7 +295,7 @@ fun Blockchain.toCoinId(): String { Blockchain.EthereumClassic, Blockchain.EthereumClassicTestnet -> "ethereum-classic" Blockchain.Stellar, Blockchain.StellarTestnet -> "stellar" Blockchain.Cardano -> "cardano" - Blockchain.Polygon, Blockchain.PolygonTestnet -> "matic-network" + Blockchain.Polygon, Blockchain.PolygonTestnet -> "polygon-ecosystem-token" Blockchain.Arbitrum, Blockchain.ArbitrumTestnet -> "arbitrum-one" Blockchain.Avalanche, Blockchain.AvalancheTestnet -> "avalanche-2" Blockchain.Solana, Blockchain.SolanaTestnet -> "solana" @@ -360,9 +366,10 @@ fun Blockchain.toCoinId(): String { Blockchain.Sui, Blockchain.SuiTestnet -> "sui" Blockchain.EnergyWebChain, Blockchain.EnergyWebChainTestnet -> "energy-web-token" Blockchain.EnergyWebX, Blockchain.EnergyWebXTestnet -> "energy-web-token" - Blockchain.Casper -> "add_later" - Blockchain.CasperTestnet -> "add_later_test" + Blockchain.Casper, Blockchain.CasperTestnet -> "casper-network" Blockchain.Core, Blockchain.CoreTestnet -> "coredaoorg" + Blockchain.Xodex -> "xodex" + Blockchain.Canxium -> "canxium" } } @@ -386,6 +393,7 @@ fun Blockchain.amountToCreateAccount(token: Token? = null): BigDecimal? { Blockchain.Near, Blockchain.NearTestnet -> 0.00182.toBigDecimal() Blockchain.Aptos, Blockchain.AptosTestnet, Blockchain.Filecoin, + Blockchain.Casper, Blockchain.CasperTestnet, -> BigDecimal.ZERO else -> null } @@ -403,6 +411,4 @@ private val excludedBlockchains = listOf( Blockchain.Unknown, Blockchain.Nexa, Blockchain.NexaTestnet, - Blockchain.Casper, - Blockchain.CasperTestnet, ) \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 1c340dabc3..c2205435c2 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -21,8 +21,8 @@ object BlockchainUtils { private const val XRP_X_ADDRESS = 'X' /** Decodes XRP Blockchain address */ - fun decodeRippleXAddress(xAddress: String, networkId: String): XrpTaggedAddress? { - return if (networkId == Blockchain.XRP.id && xAddress.firstOrNull() == XRP_X_ADDRESS) { + fun decodeRippleXAddress(xAddress: String, blockchainId: String): XrpTaggedAddress? { + return if (blockchainId == Blockchain.XRP.id && xAddress.firstOrNull() == XRP_X_ADDRESS) { val decodedAddress = XrpAddressService.decodeXAddress(xAddress) return decodedAddress?.let(XrpTaggedAddressConverter()::convert) } else { @@ -31,63 +31,68 @@ object BlockchainUtils { } /** If current [networkId] is Bitcoin */ - fun isBitcoin(networkId: String): Boolean { - val blockchain = Blockchain.fromId(networkId) + fun isBitcoin(blockchainId: String): Boolean { + val blockchain = Blockchain.fromId(blockchainId) return blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet } - /** If current [networkId] is Tezos */ - fun isTezos(networkId: String): Boolean { - val blockchain = Blockchain.fromId(networkId) + /** If current [blockchainId] is Tezos */ + fun isTezos(blockchainId: String): Boolean { + val blockchain = Blockchain.fromId(blockchainId) return blockchain == Blockchain.Tezos } - fun isCardano(networkId: String): Boolean { - val blockchain = Blockchain.fromId(networkId) + fun isCardano(blockchainId: String): Boolean { + val blockchain = Blockchain.fromId(blockchainId) return blockchain == Blockchain.Cardano } - /** If current [networkId] is BeaconChain */ - fun isBeaconChain(networkId: String): Boolean { - val blockchain = Blockchain.fromId(networkId) + /** If current [blockchainId] is BeaconChain */ + fun isBeaconChain(blockchainId: String): Boolean { + val blockchain = Blockchain.fromId(blockchainId) return blockchain == Blockchain.Binance || blockchain == Blockchain.BinanceTestnet } - /** If current [networkId] is Polygon */ - fun isPolygonChain(networkId: String): Boolean { - val blockchain = Blockchain.fromId(networkId) + /** If current [blockchainId] is Polygon */ + fun isPolygonChain(blockchainId: String): Boolean { + val blockchain = Blockchain.fromId(blockchainId) return blockchain == Blockchain.Polygon || blockchain == Blockchain.PolygonTestnet } - fun isTron(networkId: String): Boolean { - val blockchain = Blockchain.fromId(networkId) + fun isTron(blockchainId: String): Boolean { + val blockchain = Blockchain.fromId(blockchainId) return blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet } - fun isSupportedNetworkId(networkId: String): Boolean { - return Blockchain.fromNetworkId(networkId)?.isSupportedInApp() ?: false + fun isSupportedNetworkId(blockchainId: String): Boolean { + return Blockchain.fromNetworkId(blockchainId)?.isSupportedInApp() ?: false } - fun isArbitrum(networkId: String): Boolean { - val blockchain = Blockchain.fromId(networkId) + fun isArbitrum(blockchainId: String): Boolean { + val blockchain = Blockchain.fromId(blockchainId) return blockchain == Blockchain.Arbitrum } - fun isSolana(networkId: String): Boolean { - val blockchain = Blockchain.fromId(networkId) + fun isSolana(blockchainId: String): Boolean { + val blockchain = Blockchain.fromId(blockchainId) return blockchain == Blockchain.Solana } - fun isPolkadot(networkId: String): Boolean { - val blockchain = Blockchain.fromId(networkId) + fun isPolkadot(blockchainId: String): Boolean { + val blockchain = Blockchain.fromId(blockchainId) return blockchain == Blockchain.Polkadot || blockchain == Blockchain.PolkadotTestnet } - fun isCosmos(networkId: String): Boolean { - val blockchain = Blockchain.fromId(networkId) + fun isCosmos(blockchainId: String): Boolean { + val blockchain = Blockchain.fromId(blockchainId) return blockchain == Blockchain.Cosmos || blockchain == Blockchain.CosmosTestnet } + fun isBSC(blockchainId: String): Boolean { + val blockchain = Blockchain.fromId(blockchainId) + return blockchain == Blockchain.BSC || blockchain == Blockchain.BSCTestnet + } + data class BlockchainInfo( val blockchainId: String, val name: String, diff --git a/libs/tangem-sdk-api/.gitignore b/libs/tangem-sdk-api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/libs/tangem-sdk-api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libs/tangem-sdk-api/build.gradle.kts b/libs/tangem-sdk-api/build.gradle.kts new file mode 100644 index 0000000000..91756f3226 --- /dev/null +++ b/libs/tangem-sdk-api/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.legacy" +} + +dependencies { + implementation(projects.common) + implementation(projects.domain.models) + implementation(projects.domain.card) + implementation(projects.domain.legacy) + implementation(projects.domain.wallets.models) + + implementation(projects.core.res) + + /** Tangem libraries */ + implementation(deps.tangem.card.core) + implementation(deps.tangem.card.android) { + exclude(module = "joda-time") + } + + /** Other libraries */ + implementation(deps.timber) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/CreateProductWalletTaskResponse.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/CreateProductWalletTaskResponse.kt new file mode 100644 index 0000000000..e57c41c193 --- /dev/null +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/CreateProductWalletTaskResponse.kt @@ -0,0 +1,24 @@ +package com.tangem.sdk.api + +import com.tangem.common.card.Card +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.KeyWalletPublicKey +import com.tangem.operations.CommandResponse +import com.tangem.operations.backup.PrimaryCard +import com.tangem.operations.derivation.ExtendedPublicKeysMap + +data class CreateProductWalletTaskResponse( + val card: CardDTO, + val derivedKeys: Map = mapOf(), + val primaryCard: PrimaryCard? = null, +) : CommandResponse { + constructor( + card: Card, + derivedKeys: Map = mapOf(), + primaryCard: PrimaryCard? = null, + ) : this( + card = CardDTO(card), + derivedKeys = derivedKeys, + primaryCard = primaryCard, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt similarity index 93% rename from app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt rename to libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index 49bef8a287..a2c6cc8c4f 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.domain.sdk +package com.tangem.sdk.api import androidx.annotation.DrawableRes import androidx.annotation.StringRes @@ -19,7 +19,6 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.DerivationTaskResponse import com.tangem.operations.preflightread.PreflightReadFilter import com.tangem.operations.wallet.CreateWalletResponse -import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse @Suppress("TooManyFunctions") interface TangemSdkManager { @@ -96,8 +95,8 @@ interface TangemSdkManager { ): CompletionResult @Deprecated( - "TangemSdkManager shouldn't run custom tasks. " + - "All of them should be specified in TangemSdkManager certain methods.", + "com.tangem.sdk.api.TangemSdkManager shouldn't run custom tasks. " + + "All of them should be specified in com.tangem.sdk.api.TangemSdkManager certain methods.", ) suspend fun runTaskAsync( runnable: CardSessionRunnable, @@ -111,7 +110,7 @@ interface TangemSdkManager { @Suppress("MagicNumber") fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse?) - @Deprecated("TangemSdkManager shouldn't returns a string from resources") + @Deprecated("com.tangem.sdk.api.TangemSdkManager shouldn't returns a string from resources") fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TapErrors.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TapErrors.kt new file mode 100644 index 0000000000..b4fefc6ca6 --- /dev/null +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TapErrors.kt @@ -0,0 +1,49 @@ +package com.tangem.sdk.api + +import androidx.annotation.StringRes +import com.tangem.common.core.TangemError +import com.tangem.legacy.R + +interface TapErrors + +interface ArgError { + val args: List? +} + +interface MultiMessageError : TapErrors { + val errorList: List + val builder: (List) -> String +} + +sealed class TapError( + @StringRes val messageResource: Int, + override val args: List? = null, +) : Throwable(), TapErrors, ArgError { + + object UnknownError : TapError(R.string.send_error_unknown) + open class CustomError(val customMessage: String) : TapError(R.string.common_custom_string, listOf(customMessage)) + + object NoInternetConnection : TapError(R.string.wallet_notification_no_internet) + + sealed class WalletManager { + class NoAccountError(amountToCreateAccount: String) : CustomError(amountToCreateAccount) + class InternalError(message: String) : CustomError(message) + object BlockchainIsUnreachableTryLater : TapError(R.string.wallet_balance_blockchain_unreachable_try_later) + } +} + +sealed class TapSdkError(override val messageResId: Int?) : TangemError(code = 50100) { + override var customMessage: String = code.toString() + + object CardForDifferentApp : TapSdkError(R.string.alert_unsupported_card) + object CardNotSupportedByRelease : TapSdkError(R.string.error_wrong_card_type) +} + +fun TapErrors.assembleErrors(): MutableList?>> { + val idList = mutableListOf?>>() + when (this) { + is MultiMessageError -> this.errorList.forEach { idList.addAll(it.assembleErrors()) } + is TapError -> idList.add(Pair(this.messageResource, this.args)) + } + return idList +} \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index 58ea63d4a6..fb3e3e15a1 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -39,7 +39,6 @@ private fun AppExtension.configureDefaultConfig(project: Project) { testInstrumentationRunner = "com.tangem.common.HiltTestRunner" } - } // TODO: [REDACTED_JIRA] @@ -105,6 +104,8 @@ private fun AppExtension.configurePackagingOptions() { excludes += "lib/x86_64/freebsd/libscrypt.so" excludes += "lib/x86_64/linux/libscrypt.so" excludes += "META-INF/gradle/incremental.annotation.processors" + + merges += "paymentrequest.proto" } } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index e8e6fe3fcf..f915fb1945 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -147,10 +147,13 @@ include(":libs:auth") include(":libs:blockchain-sdk") include(":libs:crypto") include(":libs:visa") +include(":libs:tangem-sdk-api") // endregion Libs modules // region Feature modules include(":features:onboarding") +include(":features:onboarding-v2:api") +include(":features:onboarding-v2:impl") include(":features:referral:data") include(":features:referral:domain") @@ -198,6 +201,9 @@ include(":features:wallet-settings:impl") include(":features:markets:api") include(":features:markets:impl") + +include(":features:onramp:api") +include(":features:onramp:impl") // endregion Feature modules // region Domain modules diff --git a/version.properties b/version.properties index e7ffe36363..3a969ad8fe 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -versionName=5.17.0 \ No newline at end of file +versionName=5.18.0 \ No newline at end of file