From 5855b3012f449864c426232eb428b64026fcb416 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Mar 2026 16:29:44 +0300 Subject: [PATCH 01/75] Updated on 2026-08-14 --- .../com/tangem/common/extensions/KNode.kt | 10 ++++++-- .../MainScreenActionButtonsTest.kt | 25 ++++++++++--------- .../send/addressScreen/RecentBlockTest.kt | 3 ++- .../addressScreen/SendAddressScreenTest.kt | 2 ++ .../confirmScreen/SendConfirmScreenTest.kt | 13 +++++++++- .../tests/send/feeScreen/SendFeeScreenTest.kt | 13 +++++++--- .../send/warnings/PolkadotWarningsTest.kt | 3 ++- .../tangem/tests/swap/SearchAndSwapTest.kt | 6 +++++ 8 files changed, 54 insertions(+), 21 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt index f57b43288a..3e5c61cf6b 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt @@ -1,5 +1,6 @@ package com.tangem.common.extensions +import android.os.SystemClock import androidx.compose.ui.test.ComposeTimeoutException import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.ComposeTestRule @@ -61,9 +62,14 @@ fun KNode.clickAndWaitFor( fun KNode.performTextInputInChunks( text: String, - chunkSize: Int = 2 + chunkSize: Int = 2, + delayBetweenChunksMs: Long = 100 ) { - text.chunked(chunkSize).forEach { chunk -> + val chunks = text.chunked(chunkSize) + chunks.forEachIndexed { index, chunk -> performTextInput(chunk) + if (index < chunks.lastIndex) { + SystemClock.sleep(delayBetweenChunksMs) + } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt index 8069b5d735..2cb635f684 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt @@ -6,6 +6,7 @@ import com.tangem.common.annotations.ApiEnv import com.tangem.common.annotations.ApiEnvConfig import com.tangem.common.constants.TestConstants.BITCOIN_ADDRESS import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT +import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG import com.tangem.common.extensions.* import com.tangem.common.utils.assertClipboardTextEquals import com.tangem.common.utils.clearClipboard @@ -599,22 +600,22 @@ class MainScreenActionButtonsTest : BaseTestCase() { step("Reset Wiremock scenario: '$scenarioName'") { resetWireMockScenarioState(scenarioName) } - step("Perform pull to refresh") { - pullToRefresh(steps = 10) - waitForIdle() - } - step("Assert action buttons is enabled") { - assertActionButtonsForMultiCurrencyWallet(isEnabled = true) + step("Pull to refresh and wait for buttons to become enabled") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG, intervalMs = 2_000) { + pullToRefresh(10) + waitForIdle() + assertActionButtonsForMultiCurrencyWallet(isEnabled = true) + } } step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) } - step("Perform pull to refresh") { - pullToRefresh(steps = 10) - waitForIdle() - } - step("Assert action buttons is not enabled") { - assertActionButtonsForMultiCurrencyWallet(isEnabled = false) + step("Pull to refresh and wait for buttons to become disabled") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG, intervalMs = 2_000) { + pullToRefresh(10) + waitForIdle() + assertActionButtonsForMultiCurrencyWallet(isEnabled = false) + } } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt index d7f75da532..37950eb1a3 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/RecentBlockTest.kt @@ -104,6 +104,7 @@ class RecentBlockTest : BaseTestCase() { @Test fun recentBlockTransactionHistoryDoesNotSupportedTest() { val tokenName = "Polkadot" + val fullTokenName = "Polkadot Asset Hub" val sendAmount = "1" setupHooks( @@ -113,7 +114,7 @@ class RecentBlockTest : BaseTestCase() { } ).run { step("Open 'Send Screen' with token: $tokenName") { - openSendScreen(tokenName) + openSendScreen(tokenName = fullTokenName, mockState = tokenName) } step("Type '$sendAmount' in input text field") { onSendScreen { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/SendAddressScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/SendAddressScreenTest.kt index 32177f7b76..6f0000086b 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/SendAddressScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/addressScreen/SendAddressScreenTest.kt @@ -27,6 +27,7 @@ import dagger.hilt.android.testing.HiltAndroidTest import io.github.kakaocup.kakao.common.utilities.getResourceString import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Ignore import org.junit.Test @HiltAndroidTest @@ -98,6 +99,7 @@ class SendAddressScreenTest : BaseTestCase() { } } + @Ignore("TODO: [REDACTED_JIRA]") @AllureId("4543") @DisplayName("Send (address screen): check address field") @Test diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/confirmScreen/SendConfirmScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/confirmScreen/SendConfirmScreenTest.kt index 69307ea8b4..a9746ca3c1 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/confirmScreen/SendConfirmScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/confirmScreen/SendConfirmScreenTest.kt @@ -17,6 +17,7 @@ import dagger.hilt.android.testing.HiltAndroidTest import io.github.kakaocup.kakao.common.utilities.getResourceString import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Ignore import org.junit.Test @HiltAndroidTest @@ -152,7 +153,11 @@ class SendConfirmScreenTest : BaseTestCase() { step("Type recipient address") { onSendAddressScreen { addressTextField.performTextReplacement(recipientAddress) } } + step("Assert 'Addresses shimmer' is not displayed") { + onSendAddressScreen { addressesShimmer.assertIsNotDisplayed() } + } step("Click on 'Next' button") { + waitForIdle() onSendAddressScreen { nextButton.clickWithAssertion() } } step("Assert primary amount = '$tokenAmount'") { @@ -237,11 +242,13 @@ class SendConfirmScreenTest : BaseTestCase() { } } + @Ignore("TODO: [REDACTED_JIRA]") @AllureId("554") @DisplayName("Send (Confirm screen): check fee warning") @Test fun checkFeeWarningTest() { val tokenName = "Polkadot" + val fullTokenName = "Polkadot Asset Hub" val tokenAmount = "0.1" val warningTitle = getResourceString(R.string.send_fee_unreachable_error_title) val warningMessageResId = R.string.send_fee_unreachable_error_text @@ -256,7 +263,7 @@ class SendConfirmScreenTest : BaseTestCase() { } ).run { step("Open 'Send Screen' with token: $tokenName") { - openSendScreen(tokenName) + openSendScreen(tokenName = fullTokenName, mockState = tokenName) } step("Type '$tokenAmount' in input text field") { onSendScreen { @@ -274,7 +281,11 @@ class SendConfirmScreenTest : BaseTestCase() { step("Type address in input text field") { onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) } } + step("Assert 'Addresses shimmer' is not displayed") { + onSendAddressScreen { addressesShimmer.assertIsNotDisplayed() } + } step("Click on 'Next' button") { + waitForIdle() onSendAddressScreen { nextButton.clickWithAssertion() } } step("Assert 'Network fee info unreachable' warning title is displayed") { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt index 31b9035761..b9f6635d09 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/feeScreen/SendFeeScreenTest.kt @@ -7,6 +7,7 @@ import com.tangem.common.constants.TestConstants.POLKADOT_RECIPIENT_ADDRESS import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO import com.tangem.common.constants.TestConstants.TERRA_RECIPIENT_ADDRESS import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG import com.tangem.common.extensions.SwipeDirection import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.swipeVertical @@ -68,6 +69,7 @@ class SendFeeScreenTest : BaseTestCase() { @Test fun checkFeeBlockForFixedFeeTest() { val tokenName = "Polkadot" + val fullTokenName = "Polkadot Asset Hub" val tokenAmount = "1" val feeAmount = "$0.05" @@ -78,7 +80,7 @@ class SendFeeScreenTest : BaseTestCase() { } ).run { step("Open 'Send' screen") { - openSendScreen(tokenName) + openSendScreen(tokenName = fullTokenName, mockState = tokenName) } step("Type '$tokenAmount' in input text field") { onSendScreen { @@ -93,10 +95,13 @@ class SendFeeScreenTest : BaseTestCase() { onSendAddressScreen { addressTextField.performTextReplacement(POLKADOT_RECIPIENT_ADDRESS) } } step("Click on 'Next' button") { - onSendScreen { nextButton.clickWithAssertion() } + waitForIdle() + onSendAddressScreen { nextButton.clickWithAssertion() } } step("Assert fee block is displayed without fee selector") { - checkNetworkFeeBlock(currentFeeAmount = feeAmount, withFeeSelector = false) + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + checkNetworkFeeBlock(currentFeeAmount = feeAmount, withFeeSelector = false) + } } step("Click on 'Fee selector' block") { onSendConfirmScreen { feeSelectorBlock.performClick() } @@ -161,7 +166,7 @@ class SendFeeScreenTest : BaseTestCase() { onSendAddressScreen { addressTextField.performTextReplacement(ETHEREUM_RECIPIENT_ADDRESS) } } step("Click on 'Next' button") { - onSendScreen { nextButton.clickWithAssertion() } + onSendAddressScreen { nextButton.clickWithAssertion() } } step("Assert fee block is displayed with fee selector") { checkNetworkFeeBlock(currentFeeAmount = feeAmount, withFeeSelector = true) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt index 31cf64adce..1c481a80fb 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt @@ -21,6 +21,7 @@ import org.junit.Test @HiltAndroidTest class PolkadotWarningsTest : BaseTestCase() { private val tokenName = "Polkadot" + private val fullTokenName = "Polkadot Asset Hub" private val amountToLeaveLessThanDeposit = "1.299" private val amountToLeaveGreaterThanDeposit = "0.2" private val depositAmount = "DOT 0.01" @@ -41,7 +42,7 @@ class PolkadotWarningsTest : BaseTestCase() { } ).run { step("Open 'Send Screen' with token: $tokenName") { - openSendScreen(tokenName) + openSendScreen(tokenName = fullTokenName, mockState = tokenName) } step("Type '$amountToLeaveLessThanDeposit' in input text field") { onSendScreen { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SearchAndSwapTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SearchAndSwapTest.kt index ce2450ee04..e1e79b96ad 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/swap/SearchAndSwapTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SearchAndSwapTest.kt @@ -16,11 +16,13 @@ import dagger.hilt.android.testing.HiltAndroidTest import io.github.kakaocup.kakao.common.utilities.getResourceString import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Ignore import org.junit.Test @HiltAndroidTest class SearchAndSwapTest : BaseTestCase() { + @Ignore("ToDo: [REDACTED_JIRA]") @AllureId("8520") @DisplayName("Search and Swap: add token without derivation") @Test @@ -68,6 +70,7 @@ class SearchAndSwapTest : BaseTestCase() { } } + @Ignore("ToDo: [REDACTED_JIRA]") @AllureId("8519") @DisplayName("Search and Swap: add token with derivation") @Test @@ -115,6 +118,7 @@ class SearchAndSwapTest : BaseTestCase() { } } + @Ignore("ToDo: [REDACTED_JIRA]") @AllureId("8523") @DisplayName("Search and Swap: Markets error") @Test @@ -155,6 +159,7 @@ class SearchAndSwapTest : BaseTestCase() { } } + @Ignore("ToDo: [REDACTED_JIRA]") @AllureId("8522") @DisplayName("Search and Swap: check 'Unsupported token pair' warning") @Test @@ -205,6 +210,7 @@ class SearchAndSwapTest : BaseTestCase() { } } + @Ignore("ToDo: [REDACTED_JIRA]") @AllureId("8521") @DisplayName("Swap: search token on Swap token screen") @Test From 13a37269f54bb9d71a1d5ac877e269b1472882aa Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Mar 2026 13:25:24 +0400 Subject: [PATCH 02/75] Updated on 2026-08-14 --- .../kotlin/com/tangem/plugin/configuration/model/AppConfig.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt index 25a86d101f..4ae4d03774 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/AppConfig.kt @@ -4,6 +4,6 @@ internal object AppConfig { const val packageName = "com.tangem.wallet" const val versionCode = 1 const val minSdkVersion = 24 - const val targetSdkVersion = 35 - const val compileSdkVersion = 35 + const val targetSdkVersion = 36 + const val compileSdkVersion = 36 } \ No newline at end of file From 8b75e37e30b0ff73350b61d599f0ac269820d6d5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Mar 2026 19:10:03 +0200 Subject: [PATCH 03/75] Updated on 2026-08-14 --- .../deeplink/DefaultDeeplinkLauncher.kt | 56 +++++++++++++++++++ .../java/com/tangem/tap/di/UtilsModule.kt | 7 ++- .../com/tangem/common/routing/LinkHandler.kt | 35 ------------ .../navigation/deeplink/DeeplinkLauncher.kt | 11 ++++ features/promo-banners/impl/build.gradle.kts | 4 +- .../impl/model/PromoBannersBlockModel.kt | 6 +- 6 files changed, 75 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/common/deeplink/DefaultDeeplinkLauncher.kt delete mode 100644 common/routing/src/main/kotlin/com/tangem/common/routing/LinkHandler.kt create mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/deeplink/DeeplinkLauncher.kt diff --git a/app/src/main/java/com/tangem/tap/common/deeplink/DefaultDeeplinkLauncher.kt b/app/src/main/java/com/tangem/tap/common/deeplink/DefaultDeeplinkLauncher.kt new file mode 100644 index 0000000000..3c6fa7e0c9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/deeplink/DefaultDeeplinkLauncher.kt @@ -0,0 +1,56 @@ +package com.tangem.tap.common.deeplink + +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.tangem.common.routing.DeepLinkScheme +import com.tangem.core.navigation.deeplink.DeeplinkLauncher +import com.tangem.core.navigation.url.UrlOpener +import androidx.core.net.toUri +import timber.log.Timber + +/** + * [DeeplinkLauncher] implementation that launches deep links as intents to the current Activity + * and opens web URLs in the browser via [UrlOpener]. + */ +internal class DefaultDeeplinkLauncher( + private val context: Context, + private val urlOpener: UrlOpener, +) : DeeplinkLauncher { + + override fun launch(link: String) { + val deeplinkUri = link.toUri() + when (deeplinkUri.scheme) { + DeepLinkScheme.Tangem.scheme, + DeepLinkScheme.WalletConnect.scheme, + -> launchDeepLink(deeplinkUri) + DeepLinkScheme.Https.scheme -> launchDeeplinkOrOpenBrowser(deeplinkUri, link) + else -> { + Timber.i( + """ + No match found for deep link + |- Received URI: $deeplinkUri + """.trimIndent(), + ) + } + } + } + + private fun launchDeeplinkOrOpenBrowser(uri: Uri, link: String) { + val intent = createDeepLinkIntent(uri) + if (intent.resolveActivity(context.packageManager) != null) { + context.startActivity(intent) + } else { + urlOpener.openUrl(link) + } + } + + private fun launchDeepLink(uri: Uri) { + context.startActivity(createDeepLinkIntent(uri)) + } + + private fun createDeepLinkIntent(uri: Uri): Intent = Intent(Intent.ACTION_VIEW, uri).apply { + setPackage(context.packageName) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt index 65e36a1536..0021307180 100644 --- a/app/src/main/java/com/tangem/tap/di/UtilsModule.kt +++ b/app/src/main/java/com/tangem/tap/di/UtilsModule.kt @@ -1,8 +1,8 @@ package com.tangem.tap.di import android.content.Context -import com.tangem.common.routing.AppRouter -import com.tangem.common.routing.LinkHandler +import com.tangem.tap.common.deeplink.DefaultDeeplinkLauncher +import com.tangem.core.navigation.deeplink.DeeplinkLauncher import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.navigation.share.ShareManager @@ -49,6 +49,7 @@ internal interface UtilsModule { @Provides @Singleton - fun provideLinkHandler(appRouter: AppRouter): LinkHandler = LinkHandler(appRouter) + fun provideDeeplinkLauncher(@ApplicationContext context: Context, urlOpener: UrlOpener): DeeplinkLauncher = + DefaultDeeplinkLauncher(context, urlOpener) } } \ No newline at end of file diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/LinkHandler.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/LinkHandler.kt deleted file mode 100644 index 094ecd23eb..0000000000 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/LinkHandler.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.common.routing - -import android.net.Uri -import timber.log.Timber - -/** - * Routes in-app content links (deep links and external URLs) - * - `tangem://` scheme → parsed to AppRoute and pushed via AppRouter - * - `https://` / `http://` → opened in external browser via UrlOpener - * - Unknown scheme → logged and ignored - */ -class LinkHandler( - private val appRouter: AppRouter, -) { - - fun navigate(link: String) { - val uri = Uri.parse(link) - handleTangemDeepLink(uri) - } - - private fun handleTangemDeepLink(uri: Uri) { - val route = parseDeepLinkToRoute(uri) - if (route != null) { - appRouter.push(route) - } else { - Timber.w("ContentLinkHandler: unrecognized tangem deep link: %s", uri) - } - } - - @Suppress("UnusedParameter", "FunctionOnlyReturningConstant") - private fun parseDeepLinkToRoute(uri: Uri): AppRoute? { - // TODO [REDACTED_TASK_KEY] refactor deepling routing - return null - } -} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/deeplink/DeeplinkLauncher.kt b/core/navigation/src/main/java/com/tangem/core/navigation/deeplink/DeeplinkLauncher.kt new file mode 100644 index 0000000000..daa8fee01b --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/deeplink/DeeplinkLauncher.kt @@ -0,0 +1,11 @@ +package com.tangem.core.navigation.deeplink + +/** + * Routes in-app content links to the appropriate destination. + * - Deep link schemes (e.g. `tangem://`, `wc://`) → handled in-app + * - Web URLs (`https://`) → opened in browser + */ +interface DeeplinkLauncher { + + fun launch(link: String) +} \ No newline at end of file diff --git a/features/promo-banners/impl/build.gradle.kts b/features/promo-banners/impl/build.gradle.kts index 0bb6f452b2..a93ef194f0 100644 --- a/features/promo-banners/impl/build.gradle.kts +++ b/features/promo-banners/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { /** Core */ implementation(projects.core.decompose) + implementation(projects.core.navigation) implementation(projects.core.ui) implementation(projects.core.analytics) implementation(projects.core.analytics.models) @@ -27,9 +28,6 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.configToggles) - /** Common */ - implementation(projects.common.routing) - /** Compose */ implementation(deps.compose.foundation) implementation(deps.compose.ui) diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt index 7a9a54c6db..3187d8e91c 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt @@ -1,6 +1,6 @@ package com.tangem.features.promobanners.impl.model -import com.tangem.common.routing.LinkHandler +import com.tangem.core.navigation.deeplink.DeeplinkLauncher import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -25,7 +25,7 @@ internal class PromoBannersBlockModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, private val repository: PromoBannersRepository, - private val linkHandler: LinkHandler, + private val deeplinkLauncher: DeeplinkLauncher, private val analyticsEventHandler: AnalyticsEventHandler, private val userWalletsListRepository: UserWalletsListRepository, ) : Model() { @@ -95,7 +95,7 @@ internal class PromoBannersBlockModel @Inject constructor( private fun onButtonClick(displayId: String, deeplink: String?) { analyticsEventHandler.send(PromoBannerAnalyticsEvent.Clicked(displayId, placeholder)) - deeplink?.let { linkHandler.navigate(it) } + deeplink?.let { deeplinkLauncher.launch(it) } } private fun onBannerDismiss(walletId: String, displayId: String) { From a9bca131c3b8ec5ce1444ec978674d260746d364 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Mar 2026 21:54:46 +0400 Subject: [PATCH 04/75] Updated on 2026-08-14 --- app/build.gradle.kts | 3 +- .../ApplicationInjectionExecutionRule.kt | 13 ++-- .../tangem/common/rules/ApiEnvironmentRule.kt | 10 +-- .../com/tangem/common/utils/NetworkUtils.kt | 32 +++++----- .../com/tangem/common/utils/WireMockUtils.kt | 44 ++++++------- .../FirebasePushNotificationsTokenProvider.kt | 4 +- .../com/tangem/tap/GoogleReviewManager.kt | 8 +-- .../HuaweiPushNotificationsTokenProvider.kt | 8 +-- .../java/com/tangem/tap/HuaweiPushService.kt | 6 +- .../tangem/tap/ForegroundActivityObserver.kt | 10 +-- .../java/com/tangem/tap/LockTimerWorker.kt | 6 +- .../com/tangem/tap/LockUserWalletsTimer.kt | 10 +-- .../main/java/com/tangem/tap/MainActivity.kt | 14 ++--- .../java/com/tangem/tap/TangemApplication.kt | 8 +-- .../tangem/tap/WindowObscurationObserver.kt | 6 +- .../common/analytics/AnalyticsEventsLogger.kt | 8 +-- .../appsflyer/AppsFlyerDeepLinkListener.kt | 6 +- .../AppsFlyerReferralParamsHandler.kt | 12 ++-- .../appsflyer/TangemAFConversionListener.kt | 10 +-- .../appsflyer/AppsFlyerAnalyticsClient.kt | 18 +++--- .../appsflyer/AppsFlyerAnalyticsHandler.kt | 18 +++--- .../handlers/customerio/CustomerIoClient.kt | 4 +- .../customerio/CustomerIoLogClient.kt | 6 +- .../firebase/FirebaseAppInstanceIdProvider.kt | 6 +- .../clipboard/DefaultClipboardManager.kt | 6 +- .../common/clipboard/MockClipboardManager.kt | 4 +- .../deeplink/DefaultDeeplinkLauncher.kt | 6 +- .../tap/common/extensions/WalletManager.kt | 4 +- .../java/com/tangem/tap/common/images/Coil.kt | 16 ++--- .../common/log/TangemAppLoggerInitializer.kt | 19 ++---- .../pushes/TangemPushNotificationService.kt | 4 +- .../tangem/tap/common/redux/LogMiddleware.kt | 4 +- .../tap/common/url/CustomTabsUrlOpener.kt | 4 +- .../tap/core/DefaultAppCoroutineScope.kt | 4 +- .../navigation/email/AndroidEmailSender.kt | 6 +- .../DefaultDeviceSecurityInfoProvider.kt | 4 +- .../tap/data/TangemBlockchainSDKLogger.kt | 4 +- .../tasks/visa/VisaCardActivationTask.kt | 38 +++++------ ...erWalletsSensitiveInformationRepository.kt | 4 +- .../tap/domain/visa/VisaCardScanHandler.kt | 43 ++++++------- .../details/redux/DetailsMiddleware.kt | 4 +- .../cardsettings/model/CardSettingsModel.kt | 4 +- .../ui/resetcard/model/ResetCardModel.kt | 4 +- .../tangem/tap/features/main/MainViewModel.kt | 20 +++--- .../moonpay/MoonPayService.kt | 10 +-- .../com/tangem/tap/routing/ProxyAppRouter.kt | 10 +-- .../component/impl/DefaultRoutingComponent.kt | 4 +- .../tap/routing/utils/DeepLinkFactory.kt | 8 +-- .../tap/routing/utils/DeepLinkFactoryTest.kt | 2 - common/build.gradle.kts | 1 - common/routing/build.gradle.kts | 2 +- .../tangem/common/uri/ExternalUrlValidator.kt | 4 +- core/ab-tests/build.gradle.kts | 1 - .../manager/impl/AmplitudeABTestsManager.kt | 26 ++++---- core/config-toggles/build.gradle.kts | 1 - .../core/configtoggle/version/Version.kt | 4 +- core/datasource/build.gradle.kts | 1 - .../response/ApiResponseCallDelegate.kt | 6 +- .../api/common/response/ResponseExt.kt | 4 +- .../datasource/asset/loader/AssetLoader.kt | 59 ++++++++++------- .../datasource/di/StatusCodeInterceptor.kt | 4 +- .../local/appsflyer/DefaultAppsFlyerStore.kt | 14 ++--- .../datasource/local/logs/AppLogsStore.kt | 9 +-- .../local/preferences/PreferencesDataStore.kt | 4 +- .../utils/WireMockRedirectInterceptor.kt | 4 +- .../api/common/config/ApiConfigTest.kt | 4 +- core/navigation/build.gradle.kts | 3 +- core/res/build.gradle.kts | 1 - .../java/com/tangem/core/res/Resources.kt | 4 +- core/ui/build.gradle.kts | 1 - .../AmountVisualTransformation.kt | 4 +- core/utils/build.gradle.kts | 4 ++ .../FeatureCoroutineExceptionHandler.kt | 9 +-- .../com/tangem/utils/logging/TangemLogger.kt | 63 +++++++++++++++++++ data/account/build.gradle.kts | 1 - .../fetcher/DefaultMultiAccountListFetcher.kt | 6 +- .../FetchWalletAccountsErrorHandler.kt | 4 +- .../DefaultAccountsCRUDRepository.kt | 4 +- .../DefaultMainAccountTokensMigration.kt | 26 ++++---- data/analytics/build.gradle.kts | 2 +- data/app-currency/build.gradle.kts | 1 - .../tangem/data/balancehiding/FlipListener.kt | 4 +- data/common/build.gradle.kts | 1 - .../data/common/api/ApiResponseRaise.kt | 4 +- .../data/common/cache/DefaultCacheRegistry.kt | 12 ++-- .../common/cache/etag/DefaultETagsStore.kt | 4 +- .../common/currency/CryptoCurrencyFactory.kt | 8 +-- .../ResponseCryptoCurrenciesFactory.kt | 6 +- .../UserTokensResponseAccountIdEnricher.kt | 10 +-- .../data/common/currency/UserTokensSaver.kt | 6 +- .../data/common/network/NetworkFactory.kt | 6 +- .../data/common/quote/DefaultQuotesFetcher.kt | 4 +- .../tangem/data/common/utils/RequestUtils.kt | 4 +- data/earn/build.gradle.kts | 1 - data/express/build.gradle.kts | 1 - .../data/express/DefaultExpressRepository.kt | 4 +- .../express/DefaultExpressServiceFetcher.kt | 4 +- data/feedback/build.gradle.kts | 1 - .../feedback/DefaultFeedbackRepository.kt | 4 +- data/manage-tokens/build.gradle.kts | 1 - .../utils/ManagedCryptoCurrencyFactory.kt | 4 +- data/markets/build.gradle.kts | 1 - data/networks/build.gradle.kts | 1 - .../converters/NetworkAddressConverter.kt | 4 +- .../fetcher/CommonNetworkStatusFetcher.kt | 6 +- .../DefaultMultiNetworkStatusProducer.kt | 4 +- .../repository/DefaultNetworksRepository.kt | 16 ++--- .../store/DefaultNetworksStatusesStore.kt | 12 ++-- .../networks/store/NetworkStatusesStoreExt.kt | 6 +- .../networks/utils/DefaultNetworksCleaner.kt | 10 +-- .../networks/utils/NetworkStatusFactory.kt | 8 +-- data/news/build.gradle.kts | 1 - .../news/repository/DefaultNewsRepository.kt | 8 +-- data/nft/build.gradle.kts | 1 - .../tangem/data/nft/DefaultNFTRepository.kt | 6 +- data/onramp/build.gradle.kts | 1 - .../data/onramp/DefaultHotCryptoRepository.kt | 12 ++-- .../data/onramp/DefaultOnrampRepository.kt | 18 +++--- data/promo/build.gradle.kts | 1 - data/quotes/build.gradle.kts | 1 - .../multi/DefaultMultiQuoteStatusFetcher.kt | 8 +-- .../quotes/multi/DefaultMultiQuoteUpdater.kt | 12 ++-- .../repository/DefaultQuotesRepository.kt | 4 +- .../store/DefaultQuotesStatusesStore.kt | 6 +- data/settings/build.gradle.kts | 1 - .../settings/DefaultSettingsRepository.kt | 6 +- data/staking/build.gradle.kts | 1 - .../staking/DefaultP2PEthPoolRepository.kt | 10 +-- .../data/staking/DefaultStakeKitRepository.kt | 10 +-- ...efaultStakeKitTransactionHashRepository.kt | 6 +- .../DefaultMultiStakingBalanceFetcher.kt | 45 ++++++------- .../DefaultSingleStakingBalanceProducer.kt | 4 +- .../staking/utils/DefaultStakingCleaner.kt | 12 ++-- data/swap/build.gradle.kts | 1 - .../data/swap/DefaultSwapRepositoryV2.kt | 6 +- data/tokens/build.gradle.kts | 1 - .../data/tokens/utils/CustomTokensMerger.kt | 4 +- data/transaction/build.gradle.kts | 1 - .../DefaultTransactionRepository.kt | 12 ++-- data/txhistory/build.gradle.kts | 1 - .../repository/DefaultTxHistoryRepository.kt | 4 +- .../RefactoredTxHistoryRepository.kt | 19 +++--- .../paging/TxHistoryPagingSource.kt | 21 ++++--- data/visa/build.gradle.kts | 1 - .../DefaultTangemPayCryptoCurrencyFactory.kt | 4 +- .../DefaultPaymentAccountStatusFetcher.kt | 30 +++++---- .../DefaultTangemPayCardDetailsRepository.kt | 15 +++-- .../DefaultTangemPayWithdrawRepository.kt | 8 +-- .../repository/TangemPayRequestPerformer.kt | 6 +- .../pay/store/PaymentAccountStatusesStore.kt | 6 +- .../utils/TangemPayTxHistoryItemConverter.kt | 6 +- .../visa/utils/VisaTxHistoryPagingSource.kt | 4 +- .../initialize/DefaultWcInitializeUseCase.kt | 30 ++++----- .../solana/SolanaBlockAidAddressConverter.kt | 6 +- .../solana/WcSolanaSignTransactionUseCase.kt | 12 ++-- .../pair/DefaultWcPairUseCase.kt | 48 +++++++------- .../walletconnect/pair/WcPairSdkDelegate.kt | 6 +- .../request/DefaultWcRequestService.kt | 8 +-- .../request/DefaultWcRequestUseCaseFactory.kt | 8 +-- .../respond/DefaultWcRespondService.kt | 22 +++---- .../sessions/DefaultWcSessionsManager.kt | 8 +-- .../utils/BlockAidVerificationDelegate.kt | 8 +-- data/wallet-manager/build.gradle.kts | 1 - .../DefaultWalletManagersFacade.kt | 18 +++--- .../UpdateWalletManagerResultFactory.kt | 6 +- .../walletmanager/WalletManagerFactory.kt | 8 +-- ...TransactionDataToTxHistoryItemConverter.kt | 4 +- data/wallets/build.gradle.kts | 1 - .../wallets/DefaultWalletsPromoRepository.kt | 4 +- .../DefaultColdMapDerivationsRepository.kt | 6 +- .../DefaultDerivationsRepository.kt | 4 +- .../hot/DefaultHotMapDerivationsRepository.kt | 4 +- .../data/wallets/hot/TangemHotWalletSigner.kt | 22 +++---- data/yield-supply/build.gradle.kts | 1 - ...DefaultYieldSupplyTransactionRepository.kt | 10 +-- domain/account/status/build.gradle.kts | 3 +- .../producer/DefaultFlowProducerTools.kt | 4 +- .../usecase/ApplyTokenListSortingUseCase.kt | 4 +- .../GetAccountCurrencyByAddressUseCase.kt | 4 +- .../usecase/ManageCryptoCurrenciesUseCase.kt | 17 ++--- .../utils/CryptoCurrencyBalanceFetcher.kt | 4 +- .../status/utils/CryptoCurrencyOperations.kt | 4 +- domain/card/build.gradle.kts | 2 +- .../configs/EdSingleCurrencyCardConfig.kt | 4 +- .../domain/card/configs/GenericCardConfig.kt | 4 +- .../card/configs/MultiWalletCardConfig.kt | 4 +- .../domain/card/configs/Wallet2CardConfig.kt | 6 +- domain/legacy/build.gradle.kts | 1 - domain/staking/build.gradle.kts | 1 - .../single/SingleStakingBalanceProducer.kt | 9 ++- domain/tokens/build.gradle.kts | 1 - .../tokens/BalanceFetchingOperations.kt | 6 +- .../tokens/wallet/WalletBalanceFetcher.kt | 4 +- .../implementor/MultiWalletBalanceFetcher.kt | 6 +- domain/tokensync/build.gradle.kts | 1 - .../tokensync/usecase/SyncTokensUseCase.kt | 8 +-- domain/visa/build.gradle.kts | 1 - .../TangemPayMainScreenCustomerInfoUseCase.kt | 23 ++++--- domain/wallets/build.gradle.kts | 1 - domain/wallets/models/build.gradle.kts | 5 +- .../usecase/SyncWalletWithRemoteUseCase.kt | 4 +- domain/yield-supply/build.gradle.kts | 2 +- .../YieldSupplyEstimateEnterFeeUseCase.kt | 4 +- .../usecase/YieldSupplyPendingTracker.kt | 5 +- features/account/impl/build.gradle.kts | 1 - .../archived/ArchivedAccountListModel.kt | 4 +- .../entity/AccountArchivedUMBuilder.kt | 4 +- .../createedit/AccountCreateEditModel.kt | 6 +- features/approval/impl/build.gradle.kts | 1 - .../approval/impl/model/GiveApprovalModel.kt | 12 ++-- features/biometry/impl/build.gradle.kts | 2 +- .../biometry/impl/model/AskBiometryModel.kt | 4 +- .../impl/build.gradle.kts | 1 - .../create-wallet-start/impl/build.gradle.kts | 1 - .../CreateWalletStartModel.kt | 10 +-- features/details/impl/build.gradle.kts | 2 +- .../features/details/model/DetailsModel.kt | 6 +- .../details/model/UserWalletListModel.kt | 6 +- features/disclaimer/impl/build.gradle.kts | 1 - .../impl/ui/DisclaimerWebViewClient.kt | 8 +-- features/feed/impl/build.gradle.kts | 1 - .../add/impl/model/AddToPortfolioModel.kt | 4 +- .../AddToPortfolioPreselectedDataModel.kt | 4 +- .../model/CheckCurrencyUnsupportedDelegate.kt | 6 +- ...efaultMarketsTokenDetailDeepLinkHandler.kt | 4 +- .../DefaultNewsDetailsDeepLinkHandler.kt | 4 +- .../model/market/list/utils/LoggingUtils.kt | 12 ++-- .../model/news/details/NewsDetailsModel.kt | 6 +- features/home/impl/build.gradle.kts | 4 +- .../features/home/impl/model/HomeModel.kt | 14 ++--- features/hot-wallet/impl/build.gradle.kts | 1 - .../HotAccessCodeRequestModel.kt | 4 +- .../model/AddExistingWalletImportModel.kt | 12 ++-- .../CreateHardwareWalletModel.kt | 10 +-- .../CreateMobileWalletModel.kt | 4 +- .../forgetwallet/ForgetWalletModel.kt | 6 +- .../check/model/ManualBackupCheckModel.kt | 15 ++--- .../phrase/model/ManualBackupPhraseModel.kt | 4 +- .../viewphrase/model/ViewPhraseModel.kt | 4 +- .../walletbackup/model/WalletBackupModel.kt | 10 +-- .../model/WalletHardwareBackupModel.kt | 4 +- features/kyc/impl/build.gradle.kts | 1 - .../tangem/features/kyc/DefaultKycModel.kt | 4 +- features/manage-tokens/impl/build.gradle.kts | 2 +- .../model/ChooseManagedTokensModel.kt | 4 +- .../model/CustomTokenFormModel.kt | 6 +- .../managetokens/model/ManageTokensModel.kt | 6 +- .../model/OnboardingManageTokensModel.kt | 8 +-- .../utils/CustomCurrencyValidator.kt | 10 +-- .../list/CustomTokenFormUseCasesFacade.kt | 13 ++-- .../utils/list/ManageTokensListManager.kt | 12 ++-- features/markets/impl/build.gradle.kts | 2 +- features/nft/impl/build.gradle.kts | 1 - .../collections/model/NFTCollectionsModel.kt | 4 +- .../nft/details/model/NFTDetailsModel.kt | 4 +- features/onboarding-v2/impl/build.gradle.kts | 1 - .../v2/twin/impl/model/OnboardingTwinModel.kt | 14 ++--- features/onramp/impl/build.gradle.kts | 1 - .../onramp/alloffers/model/AllOffersModel.kt | 4 +- .../deeplink/DefaultBuyDeepLinkHandler.kt | 4 +- .../deeplink/DefaultOnrampDeepLinkHandler.kt | 4 +- .../deeplink/DefaultSellDeepLinkHandler.kt | 4 +- .../deeplink/DefaultSwapDeepLinkHandler.kt | 4 +- .../onramp/hottokens/model/HotCryptoModel.kt | 4 +- .../model/OnrampAddToPortfolioModel.kt | 8 +-- .../main/model/OnrampMainComponentModel.kt | 4 +- .../providers/model/SelectProviderModel.kt | 4 +- .../redirect/model/OnrampRedirectModel.kt | 4 +- .../model/OnrampSuccessComponentModel.kt | 10 +-- .../swap/entity/SwapSelectTokensController.kt | 6 +- .../tokenlist/entity/TokenListUMController.kt | 8 +-- features/promo-banners/impl/build.gradle.kts | 1 - .../impl/model/PromoBannersBlockModel.kt | 10 ++- .../push-notifications/impl/build.gradle.kts | 2 +- features/qr-scanning/impl/build.gradle.kts | 1 - .../qrscanning/DefaultQrScanningComponent.kt | 8 +-- features/referral/domain/build.gradle.kts | 1 - .../referral/domain/ReferralInteractorImpl.kt | 4 +- features/referral/impl/build.gradle.kts | 1 - .../DefaultReferralDeepLinkHandler.kt | 4 +- features/send-v2/impl/build.gradle.kts | 2 +- .../DefaultSellRedirectDeepLinkHandler.kt | 8 +-- .../v2/feeselector/model/FeeSelectorLogic.kt | 4 +- .../v2/send/confirm/model/SendConfirmModel.kt | 6 +- .../features/send/v2/send/model/SendModel.kt | 6 +- .../confirm/model/NFTSendConfirmModel.kt | 6 +- features/staking/impl/build.gradle.kts | 1 - .../deeplink/DefaultStakingDeepLinkHandler.kt | 8 +-- .../impl/presentation/model/StakingModel.kt | 8 +-- .../helpers/StakeKitTransactionSender.kt | 4 +- features/stories/impl/build.gradle.kts | 6 +- .../stories/impl/model/StoriesModel.kt | 4 +- features/swap-v2/impl/build.gradle.kts | 2 +- .../v2/impl/amount/model/SwapAmountModel.kt | 4 +- .../model/SwapChooseTokenNetworkModel.kt | 8 +-- .../confirm/model/SwapTransactionSender.kt | 6 +- .../sendviaswap/model/SendWithSwapModel.kt | 4 +- features/swap/data/build.gradle.kts | 1 - .../feature/swap/DefaultSwapRepository.kt | 6 +- features/swap/domain/build.gradle.kts | 1 - .../feature/swap/domain/SwapInteractorImpl.kt | 18 +++--- features/swap/impl/build.gradle.kts | 1 - .../tangem/feature/swap/model/SwapModel.kt | 24 +++---- .../tangempay/details/impl/build.gradle.kts | 2 +- .../model/TangemPayChangePinModel.kt | 4 +- .../tangempay/model/TangemPayDetailsModel.kt | 12 ++-- .../tangempay/utils/GoogleWalletUtil.kt | 9 +-- .../onboarding/impl/build.gradle.kts | 2 +- .../model/TangemPayOnboardingModel.kt | 10 +-- features/tester/impl/build.gradle.kts | 1 - .../actions/TesterActionsScreen.kt | 4 +- .../actions/TesterActionsViewModel.kt | 10 +-- .../surveysparrow/SurveySparrowManager.kt | 6 +- features/token-recieve/impl/build.gradle.kts | 1 - features/tokendetails/impl/build.gradle.kts | 1 - .../DefaultTokenDetailsDeepLinkHandler.kt | 8 +-- .../tokendetails/model/TokenDetailsModel.kt | 26 ++++---- .../TokenDetailsNotificationConverter.kt | 4 +- .../TokenDetailsStakingInfoConverter.kt | 8 ++- ...enDetailsSwapTransactionsStateConverter.kt | 4 +- .../factory/express/ExchangeStatusFactory.kt | 6 +- .../factory/express/OnrampStatusFactory.kt | 6 +- .../TokenDetailsExchangeStatusFactory.kt | 6 +- .../TokenDetailsOnrampStatusFactory.kt | 4 +- features/txhistory/impl/build.gradle.kts | 2 +- .../txhistory/model/TxHistoryModel.kt | 4 +- .../wallet-settings/impl/build.gradle.kts | 2 +- .../component/impl/model/RenameWalletModel.kt | 4 +- .../model/WalletSettingsModel.kt | 6 +- .../utils/AccountListSortingSaver.kt | 4 +- features/wallet/impl/build.gradle.kts | 1 - .../wallet/child/wallet/model/WalletModel.kt | 14 ++--- .../model/WalletsUpdateActionResolver.kt | 4 +- .../intents/WalletContentClickIntents.kt | 10 +-- .../intents/WalletWarningsClickIntents.kt | 20 +++--- .../deeplink/DefaultPromoDeeplinkHandler.kt | 32 +++++----- .../HasSingleWalletSignedHashesUseCase.kt | 4 +- .../wallet/domain/OnrampStatusFactory.kt | 4 +- .../presentation/wallet/domain/UseCaseExt.kt | 6 +- .../wallet/domain/WalletContentFetcher.kt | 12 ++-- .../domain/WalletNameMigrationUseCase.kt | 10 +-- .../loaders/WalletScreenContentLoader.kt | 12 ++-- .../wallet/state/WalletStateController.kt | 4 +- .../transformers/DeleteWalletTransformer.kt | 4 +- .../transformers/RenameWalletsTransformer.kt | 6 +- .../SetCryptoCurrencyActionsTransformer.kt | 6 +- .../SetExpressStatusesTransformer.kt | 6 +- .../SetPrimaryCurrencyTransformer.kt | 6 +- .../SetTokenListErrorTransformer.kt | 8 +-- .../transformers/SetTokenListTransformer.kt | 8 +-- .../SetTxHistoryCountErrorTransformer.kt | 4 +- .../SetTxHistoryCountTransformer.kt | 8 +-- .../SetTxHistoryItemsErrorTransformer.kt | 6 +- .../SetTxHistoryItemsTransformer.kt | 6 +- .../transformers/SetWarningsTransformer.kt | 6 +- .../transformers/UnlockWalletTransformer.kt | 6 +- .../UpdateWalletCardsCountTransformer.kt | 4 +- .../subscribers/BasicAccountListSubscriber.kt | 4 +- .../subscribers/TangemPayMainSubscriber.kt | 4 +- .../wallet/subscribers/WalletSubscriber.kt | 4 +- .../DefaultPromoDeeplinkHandlerTest.kt | 2 - features/walletconnect/impl/build.gradle.kts | 2 +- .../model/WcConnectedAppInfoModel.kt | 4 +- .../ui/preview/WcConnectionsPreviewData.kt | 22 +++---- .../DefaultWalletConnectDeepLinkHandler.kt | 6 +- .../WcTransactionRequestBlockUMConverter.kt | 4 +- features/welcome/impl/build.gradle.kts | 2 +- features/yield-supply/impl/build.gradle.kts | 2 +- .../active/model/YieldSupplyActiveModel.kt | 8 +-- .../impl/entry/model/YieldSupplyEntryModel.kt | 4 +- .../impl/main/model/YieldSupplyModel.kt | 14 ++--- .../approve/model/YieldSupplyApproveModel.kt | 16 ++--- .../model/YieldSupplyStartEarningModel.kt | 12 ++-- .../model/YieldSupplyStopEarningModel.kt | 4 +- gradle/dependencies.toml | 2 - libs/blockchain-sdk/build.gradle.kts | 1 - .../WalletManagerFactoryCreator.kt | 4 +- .../BlockchainProviderTypesConverter.kt | 4 +- .../BlockchainProvidersResponseLoader.kt | 4 +- .../BlockchainProvidersResponseMerger.kt | 4 +- .../DevBlockchainProvidersTypesManager.kt | 4 +- .../ProdBlockchainProvidersTypesManager.kt | 6 +- libs/crypto/build.gradle.kts | 1 - .../derivation/AccountNodeRecognizer.kt | 6 +- .../derivation/MutableDerivationPath.kt | 4 +- libs/tangem-sdk-api/build.gradle.kts | 1 - 386 files changed, 1369 insertions(+), 1347 deletions(-) create mode 100644 core/utils/src/main/java/com/tangem/utils/logging/TangemLogger.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a2c8978746..5379cdff1d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -369,9 +369,7 @@ dependencies { implementation(deps.googlePlay.services) implementation(deps.googlePlay.advertising) coreLibraryDesugaring(deps.desugar) - implementation(deps.timber) implementation(deps.kermit) - implementation(deps.reKotlin) implementation(deps.zxing.qrCore) implementation(deps.coil) implementation(deps.coil.gif) @@ -390,6 +388,7 @@ dependencies { implementation(deps.viewBindingDelegate) implementation(deps.armadillo) implementation(deps.kotlin.serialization) + implementation(deps.reKotlin) implementation(deps.reownCore) implementation(deps.reownWeb3) implementation(deps.prettyLogger) diff --git a/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt b/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt index 0301a65b80..fbf86435f4 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/ApplicationInjectionExecutionRule.kt @@ -8,7 +8,7 @@ import dagger.hilt.android.testing.OnComponentReadyRunner import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.lang.reflect.Field class ApplicationInjectionExecutionRule( @@ -46,7 +46,7 @@ class ApplicationInjectionExecutionRule( try { originalVersionValues = FeatureToggles.entries.associateWith { it.version } } catch (e: Exception) { - Timber.e("Failed to save original toggles values: ${e.message}") + TangemLogger.e("Failed to save original toggles values: ${e.message}") } } @@ -60,9 +60,10 @@ class ApplicationInjectionExecutionRule( versionField.set(toggle, newVersion) } - Timber.i("FeatureToggles.values updated: $toggleStates") + TangemLogger.i("FeatureToggles.values updated: $toggleStates") + } catch (e: Exception) { - Timber.e("FeatureToggles.values didn't change with error: ${e.message}") + TangemLogger.e("FeatureToggles.values didn't change with error: ${e.message}") } } @@ -75,9 +76,9 @@ class ApplicationInjectionExecutionRule( versionField.set(toggle, originalVersion) } - Timber.i("FeatureToggles.values restored") + TangemLogger.i("FeatureToggles.values restored") } catch (e: Exception) { - Timber.e("FeatureToggles.values didn't restored with error: ${e.message}") + TangemLogger.e("FeatureToggles.values didn't restored with error: ${e.message}") } } diff --git a/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt b/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt index a8d1a2c038..098d2f4def 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/rules/ApiEnvironmentRule.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.runBlocking import org.junit.rules.TestRule import org.junit.runner.Description import org.junit.runners.model.Statement -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * A JUnit rule that sets up the API environment for tests based on annotations or instrumentation arguments. @@ -82,11 +82,11 @@ class ApiEnvironmentRule : TestRule { val environment = ApiEnvironment.valueOf(parts[1]) apiConfigId to environment } catch (e: IllegalArgumentException) { - Timber.w("Invalid config or environment: $configPair") + TangemLogger.w("Invalid config or environment: $configPair") null } } else { - Timber.w("Invalid config format: $configPair. Expected format: 'ConfigId=Environment'") + TangemLogger.w("Invalid config format: $configPair. Expected format: 'ConfigId=Environment'") null } } @@ -94,7 +94,7 @@ class ApiEnvironmentRule : TestRule { DEFAULT_API_CONFIGS.associateWith { ApiEnvironment.MOCK } + parsedConfigs } catch (e: Exception) { - Timber.w("Failed to parse environment configs: $envConfigArg") + TangemLogger.w("Failed to parse environment configs: $envConfigArg") DEFAULT_API_CONFIGS.associateWith { ApiEnvironment.MOCK } } } @@ -115,7 +115,7 @@ class ApiEnvironmentRule : TestRule { runBlocking { targetEnvironments.forEach { (apiConfigId, environment) -> changeEnvironment(apiConfigId.name, environment) - Timber.i("$apiConfigId environment set to: ${environment.name}") + TangemLogger.i("$apiConfigId environment set to: ${environment.name}") } } } diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt index da1573d026..48c2a5cc5a 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt @@ -3,7 +3,7 @@ package com.tangem.common.utils import okhttp3.OkHttpClient import okhttp3.Request import org.json.JSONObject -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.util.concurrent.TimeUnit /** @@ -13,7 +13,7 @@ fun getWcUri( network: String = "ethereum", baseUrl: String = "[REDACTED_ENV_URL]" ): String? { - Timber.i("Getting WC URI for network: $network") + TangemLogger.i("Getting WC URI for network: $network") val client = OkHttpClient.Builder() .connectTimeout(30, TimeUnit.SECONDS) // Таймаут подключения @@ -29,31 +29,31 @@ fun getWcUri( return try { client.newCall(request).execute().use { response -> - Timber.i("Response code: ${response.code}") + TangemLogger.i("Response code: ${response.code}") if (response.isSuccessful) { val body = response.body?.string() ?: "" - Timber.i("Response body: $body") + TangemLogger.i("Response body: $body") val jsonObject = JSONObject(body) if (jsonObject.getBoolean("success")) { val wcUri = jsonObject.getString("wcUri") - Timber.i("Got WC URI successfully: $wcUri") + TangemLogger.i("Got WC URI successfully: $wcUri") wcUri } else { - Timber.e("API returned error: ${jsonObject.optString("error", "Unknown")}") + TangemLogger.e("API returned error: ${jsonObject.optString("error", "Unknown")}") null } } else { val errorBody = response.body?.string() ?: "No error body" - Timber.e("Request failed: ${response.code}, body: $errorBody") + TangemLogger.e("Request failed: ${response.code}, body: $errorBody") null } } } catch (e: Exception) { - Timber.e(e, "Error getting WC URI") + TangemLogger.e("Error getting WC URI", e) null } } @@ -61,7 +61,7 @@ fun getWcUri( fun checkServiceHealth( baseUrl: String = "[REDACTED_ENV_URL]" ): String? { - Timber.i("Checking service health") + TangemLogger.i("Checking service health") val client = OkHttpClient() val request = Request.Builder() @@ -71,14 +71,14 @@ fun checkServiceHealth( return try { client.newCall(request).execute().use { response -> - Timber.i("Response code: ${response.code}") + TangemLogger.i("Response code: ${response.code}") if (response.isSuccessful) { val body = response.body?.string() ?: "" - Timber.i("Response body: $body") + TangemLogger.i("Response body: $body") if (body.isEmpty()) { - Timber.e("Response body is empty") + TangemLogger.e("Response body is empty") return null } @@ -86,20 +86,20 @@ fun checkServiceHealth( val status = jsonObject.optString("status", "") if (status.isNotEmpty()) { - Timber.i("Got status successfully: $status") + TangemLogger.i("Got status successfully: $status") status } else { - Timber.e("Status field is missing or empty") + TangemLogger.e("Status field is missing or empty") null } } else { val errorBody = response.body?.string() ?: "No error body" - Timber.e("Request failed: ${response.code}, body: $errorBody") + TangemLogger.e("Request failed: ${response.code}, body: $errorBody") null } } } catch (e: Exception) { - Timber.e(e, "Error checking health") + TangemLogger.e("Error checking health", e) null } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt index 1095a7aa96..0f7a68f9b8 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/WireMockUtils.kt @@ -4,7 +4,7 @@ import com.tangem.datasource.utils.WireMockRedirectInterceptor import okhttp3.* import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.io.IOException private const val DEFAULT_WIREMOCK_URL = "[REDACTED_ENV_URL]" @@ -27,8 +27,8 @@ fun setWireMockScenarioState( state: String, baseUrl: String = getWireMockBaseUrl() ): Boolean { - Timber.i("=== WireMock Scenario Set ===") - Timber.i("Setting scenario '$scenarioName' to state: $state") + TangemLogger.i("=== WireMock Scenario Set ===") + TangemLogger.i("Setting scenario '$scenarioName' to state: $state") val client = OkHttpClient() val json = """{"state": "$state"}""" val mediaType = "application/json".toMediaType() @@ -41,14 +41,14 @@ fun setWireMockScenarioState( return try { client.newCall(request).execute().use { response -> val body = response.body?.string() ?: "" - Timber.d("WireMock scenario request URL: ${request.url}") - Timber.d("WireMock scenario request body: $json") - Timber.d("WireMock scenario response: ${response.code} - ${response.message}") - Timber.d("WireMock scenario response body: $body") + TangemLogger.d("WireMock scenario request URL: ${request.url}") + TangemLogger.d("WireMock scenario request body: $json") + TangemLogger.d("WireMock scenario response: ${response.code} - ${response.message}") + TangemLogger.d("WireMock scenario response body: $body") response.isSuccessful } } catch (e: IOException) { - Timber.e(e, "WireMock scenario error") + TangemLogger.e("WireMock scenario error", e) false } } @@ -67,12 +67,12 @@ fun checkWireMockStatus(baseUrl: String = getWireMockBaseUrl()): Boolean { return try { client.newCall(request).execute().use { response -> val body = response.body?.string() ?: "" - Timber.d("WireMock status check: ${response.code}") - Timber.d("Available scenarios: $body") + TangemLogger.d("WireMock status check: ${response.code}") + TangemLogger.d("Available scenarios: $body") response.isSuccessful } } catch (e: IOException) { - Timber.e(e, "WireMock not accessible") + TangemLogger.e("WireMock not accessible", e) false } } @@ -82,12 +82,12 @@ fun checkWireMockStatus(baseUrl: String = getWireMockBaseUrl()): Boolean { * @param baseUrl WireMock base URL (defaults to local override if set, otherwise remote) */ fun resetWireMockScenarios(baseUrl: String = getWireMockBaseUrl()): Boolean { - Timber.i("=== WireMock Scenarios Reset ===") - Timber.i("Base URL: $baseUrl") + TangemLogger.i("=== WireMock Scenarios Reset ===") + TangemLogger.i("Base URL: $baseUrl") val client = OkHttpClient() val url = "$baseUrl/__admin/scenarios/reset" - Timber.i("Request URL: $url") + TangemLogger.i("Request URL: $url") val request = Request.Builder() .url(url) @@ -95,19 +95,19 @@ fun resetWireMockScenarios(baseUrl: String = getWireMockBaseUrl()): Boolean { .build() return try { - Timber.d("Sending reset request...") + TangemLogger.d("Sending reset request...") client.newCall(request).execute().use { response -> - Timber.d("Response code: ${response.code}") - Timber.d("Response message: ${response.message}") + TangemLogger.d("Response code: ${response.code}") + TangemLogger.d("Response message: ${response.message}") val responseBody = response.body?.string() ?: "" - Timber.d("Response body: $responseBody") + TangemLogger.d("Response body: $responseBody") val isSuccessful = response.isSuccessful - Timber.d("Is successful: $isSuccessful") + TangemLogger.d("Is successful: $isSuccessful") isSuccessful } } catch (e: IOException) { - Timber.e(e, "Exception during reset") + TangemLogger.e("Exception during reset", e) false } } @@ -124,7 +124,7 @@ fun resetWireMockScenarioState( initialState: String = "Started", baseUrl: String = getWireMockBaseUrl() ): Boolean { - Timber.i("=== WireMock Scenario Reset ===") - Timber.i("Resetting scenario '$scenarioName' to initial state: $initialState") + TangemLogger.i("=== WireMock Scenario Reset ===") + TangemLogger.i("Resetting scenario '$scenarioName' to initial state: $initialState") return setWireMockScenarioState(scenarioName, initialState, baseUrl) } \ No newline at end of file diff --git a/app/src/google/java/com/tangem/tap/FirebasePushNotificationsTokenProvider.kt b/app/src/google/java/com/tangem/tap/FirebasePushNotificationsTokenProvider.kt index 627e41f2cc..39067a2ac9 100644 --- a/app/src/google/java/com/tangem/tap/FirebasePushNotificationsTokenProvider.kt +++ b/app/src/google/java/com/tangem/tap/FirebasePushNotificationsTokenProvider.kt @@ -1,9 +1,9 @@ package com.tangem.tap import com.google.firebase.messaging.FirebaseMessaging +import com.tangem.utils.logging.TangemLogger import com.tangem.utils.notifications.PushNotificationsTokenProvider import kotlinx.coroutines.tasks.await -import timber.log.Timber import javax.inject.Inject internal class FirebasePushNotificationsTokenProvider @Inject constructor() : PushNotificationsTokenProvider { @@ -11,7 +11,7 @@ internal class FirebasePushNotificationsTokenProvider @Inject constructor() : Pu return try { FirebaseMessaging.getInstance().token.await() } catch (ex: Exception) { - Timber.e(ex) + TangemLogger.e("Error", ex) "" } } diff --git a/app/src/google/java/com/tangem/tap/GoogleReviewManager.kt b/app/src/google/java/com/tangem/tap/GoogleReviewManager.kt index 956e058583..cf4724d3f4 100644 --- a/app/src/google/java/com/tangem/tap/GoogleReviewManager.kt +++ b/app/src/google/java/com/tangem/tap/GoogleReviewManager.kt @@ -5,7 +5,7 @@ import com.google.android.gms.tasks.Task import com.google.android.play.core.review.ReviewInfo import com.google.android.play.core.review.ReviewManagerFactory import com.tangem.core.navigation.review.ReviewManager -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import com.google.android.play.core.review.ReviewManager as GReviewManager /** @@ -26,7 +26,7 @@ internal class GoogleReviewManager : ReviewManager { onDismissClick = onDismissClick, ) } - .addOnFailureListener(Timber::e) + .addOnFailureListener { TangemLogger.e("Error", it) } } } @@ -42,9 +42,9 @@ internal class GoogleReviewManager : ReviewManager { .addOnCompleteListener { resultReviewTask -> if (!resultReviewTask.isSuccessful) onDismissClick() } - .addOnFailureListener(Timber::e) + .addOnFailureListener { TangemLogger.e("Error", it) } } else { - Timber.e(task.exception) + TangemLogger.e("Error", task.exception) } } } \ No newline at end of file diff --git a/app/src/huawei/java/com/tangem/tap/HuaweiPushNotificationsTokenProvider.kt b/app/src/huawei/java/com/tangem/tap/HuaweiPushNotificationsTokenProvider.kt index 03c709cd6c..1136383fb5 100644 --- a/app/src/huawei/java/com/tangem/tap/HuaweiPushNotificationsTokenProvider.kt +++ b/app/src/huawei/java/com/tangem/tap/HuaweiPushNotificationsTokenProvider.kt @@ -11,7 +11,7 @@ import com.tangem.utils.notifications.PushNotificationsTokenProvider import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject internal class HuaweiPushNotificationsTokenProvider @Inject constructor( @@ -25,7 +25,7 @@ internal class HuaweiPushNotificationsTokenProvider @Inject constructor( try { FirebaseMessaging.getInstance().token.await() } catch (ex: Exception) { - Timber.e(ex) + TangemLogger.e("Error", ex) "" } } else { @@ -33,10 +33,10 @@ internal class HuaweiPushNotificationsTokenProvider @Inject constructor( try { val appId = AGConnectOptionsBuilder().build(context).getString(APP_ID_KEY) val token = HmsInstanceId.getInstance(context).getToken(appId, TOKEN_REQUEST_MODE) - Timber.i("Requested token from HuaweiService: $token") + TangemLogger.i("Requested token from HuaweiService: $token") token } catch (e: ApiException) { - Timber.i("Fetching token from HuaweiService failed cause: ${e.message}") + TangemLogger.i("Fetching token from HuaweiService failed cause: ${e.message}") "" } } diff --git a/app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt b/app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt index b7a1de528d..cfd160dab2 100644 --- a/app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt +++ b/app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt @@ -5,7 +5,7 @@ import com.huawei.hms.push.HmsMessageService import com.huawei.hms.push.RemoteMessage import com.tangem.google.GoogleServicesHelper import com.tangem.tap.common.pushes.PushNotificationDelegate -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger class HuaweiPushService : HmsMessageService() { @@ -15,12 +15,12 @@ class HuaweiPushService : HmsMessageService() { override fun onNewToken(token: String?, bundle: Bundle?) { super.onNewToken(token, bundle) - Timber.i("HuaweiPushService: On new token from HuaweiService: $token") + TangemLogger.i("HuaweiPushService: On new token from HuaweiService: $token") } override fun onTokenError(e: Exception?, bundle: Bundle?) { super.onTokenError(e, bundle) - Timber.i("HuaweiPushService: Fetching token from HuaweiService failed cause: ${e?.message}") + TangemLogger.i("HuaweiPushService: Fetching token from HuaweiService failed cause: ${e?.message}") } override fun onMessageReceived(message: RemoteMessage?) { diff --git a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt index 30db15d90c..b0e4f21128 100644 --- a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt +++ b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt @@ -4,7 +4,7 @@ import android.app.Activity import android.app.Application.ActivityLifecycleCallbacks import android.os.Bundle import androidx.appcompat.app.AppCompatActivity -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import kotlin.reflect.KClass object ForegroundActivityObserver { @@ -14,7 +14,7 @@ object ForegroundActivityObserver { val foregroundActivity: AppCompatActivity? get() = activities.entries .firstOrNull { entry -> - Timber.i("foregroundActivity: ${entry.key} | ${entry.value.isDestroyed}") + TangemLogger.i("foregroundActivity: ${entry.key} | ${entry.value.isDestroyed}") entry.value.isDestroyed == false } ?.value @@ -27,15 +27,15 @@ object ForegroundActivityObserver { } override fun onActivityResumed(activity: Activity) { - Timber.i("onActivityResumed ${activity::class}") + TangemLogger.i("onActivityResumed ${activity::class}") if (activity is AppCompatActivity) { - Timber.i("onActivityResumed store activity") + TangemLogger.i("onActivityResumed store activity") activities[activity::class] = activity } } override fun onActivityDestroyed(activity: Activity) { - Timber.i("onActivityDestroyed") + TangemLogger.i("onActivityDestroyed") activities.remove(activity::class) } diff --git a/app/src/main/java/com/tangem/tap/LockTimerWorker.kt b/app/src/main/java/com/tangem/tap/LockTimerWorker.kt index 62c9fa652d..40aace40cd 100644 --- a/app/src/main/java/com/tangem/tap/LockTimerWorker.kt +++ b/app/src/main/java/com/tangem/tap/LockTimerWorker.kt @@ -6,9 +6,9 @@ import androidx.work.CoroutineWorker import androidx.work.WorkerParameters import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import timber.log.Timber @HiltWorker class LockTimerWorker @AssistedInject constructor( @@ -19,11 +19,11 @@ class LockTimerWorker @AssistedInject constructor( ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { - Timber.i("onStart job") + TangemLogger.i("onStart job") userWalletsListRepository.lockAllWallets().onRight { settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) } - Timber.i("onStart job complete") + TangemLogger.i("onStart job complete") return Result.success() } diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index 809f2808bc..35e3e92b9a 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -12,11 +12,11 @@ import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase import com.tangem.tap.LockTimerWorker.Companion.TAG import com.tangem.tap.common.extensions.dispatchNavigationAction +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import timber.log.Timber import java.util.concurrent.TimeUnit import kotlin.time.Duration @@ -45,7 +45,7 @@ internal class LockUserWalletsTimer( WorkManager.getInstance(context).cancelAllWorkByTag(TAG) coroutineScope.launch { val shouldOpenWelcomeScreenOnResume = settingsRepository.shouldOpenWelcomeScreenOnResume() - Timber.i( + TangemLogger.i( """ Owner resumed |- Need to open welcome screen: $shouldOpenWelcomeScreenOnResume @@ -65,7 +65,7 @@ internal class LockUserWalletsTimer( } override fun onStop(owner: LifecycleOwner) { - Timber.i("Owner stopped") + TangemLogger.i("Owner stopped") delayJob = null startTimerWorker() @@ -73,7 +73,7 @@ internal class LockUserWalletsTimer( fun restart() { if (delayJob == null) return - Timber.i( + TangemLogger.i( """ Timer restart |- Duration millis: ${duration.inWholeMilliseconds} @@ -93,7 +93,7 @@ internal class LockUserWalletsTimer( private fun start(log: Boolean = true) { if (log) { - Timber.i( + TangemLogger.i( """ Timer start |- Duration millis: ${duration.inWholeMilliseconds} diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index ea612e1a2d..dd856fb233 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -63,11 +63,11 @@ import com.tangem.tap.routing.utils.DeepLinkFactory import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import com.tangem.utils.extensions.uriValidate +import com.tangem.utils.logging.TangemLogger import com.tangem.wallet.BuildConfig import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlin.time.Duration.Companion.seconds @@ -169,7 +169,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { private val onActivityResultCallbacks = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { - Timber.i("onCreate") + TangemLogger.i("onCreate") // We need to call it before onCreate to prevent unnecessary activity recreation installAppTheme() @@ -306,18 +306,18 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { override fun onStart() { super.onStart() - Timber.i("onStart") + TangemLogger.i("onStart") dialogManager.onStart(this) } override fun onStop() { dialogManager.onStop() super.onStop() - Timber.i("onStop") + TangemLogger.i("onStop") } override fun onDestroy() { - Timber.i("onDestroy") + TangemLogger.i("onDestroy") // workaround: kill process when activity destroy to avoid state when lock() wallets // and navigation to unlock screen was skipped because system kills activity but not process if (BuildConfig.BUILD_TYPE != MOCKED_BUILD_TYPE) { @@ -416,8 +416,8 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { private fun sendStakingUnsubmittedHashes() { lifecycleScope.launch { sendUnsubmittedHashesUseCase.invoke() - .onLeft { Timber.e(it.toString()) } - .onRight { Timber.d("Submitting hashes succeeded") } + .onLeft { TangemLogger.e(it.toString()) } + .onRight { TangemLogger.d("Submitting hashes succeeded") } } } diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 5dd5a03d4c..835298b890 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -75,13 +75,13 @@ import com.tangem.tap.common.redux.appReducer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.utils.logging.TangemLogger import com.tangem.wallet.BuildConfig import dagger.hilt.EntryPoints import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import org.rekotlin.Store -import timber.log.Timber lateinit var store: Store @@ -299,10 +299,10 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. store = createReduxStore() - Timber.i("APP STARTED") + TangemLogger.i("APP STARTED") if (BuildConfig.TESTER_MENU_ENABLED) { - Timber.i(featureTogglesManager.toString()) - Timber.i(excludedBlockchainsManager.toString()) + TangemLogger.i(featureTogglesManager.toString()) + TangemLogger.i(excludedBlockchainsManager.toString()) } initWithConfigDependency(environmentConfig = environmentConfig) diff --git a/app/src/main/java/com/tangem/tap/WindowObscurationObserver.kt b/app/src/main/java/com/tangem/tap/WindowObscurationObserver.kt index 57f5a3fdec..627b631f1c 100644 --- a/app/src/main/java/com/tangem/tap/WindowObscurationObserver.kt +++ b/app/src/main/java/com/tangem/tap/WindowObscurationObserver.kt @@ -7,7 +7,7 @@ import androidx.lifecycle.LifecycleOwner import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.event.TechAnalyticsEvent import com.tangem.core.analytics.models.event.TechAnalyticsEvent.WindowObscured.ObscuredState -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal object WindowObscurationObserver : DefaultLifecycleObserver { @@ -36,7 +36,7 @@ internal object WindowObscurationObserver : DefaultLifecycleObserver { } if (isPartiallyObscured) { - Timber.d("Window is partially obscured") + TangemLogger.d("Window is partially obscured") if (!isWindowPartiallyObscuredAlreadySent) { analyticsEventHandler.send( @@ -50,7 +50,7 @@ internal object WindowObscurationObserver : DefaultLifecycleObserver { val isFullyObscured = event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0 if (isFullyObscured) { - Timber.d("Window is partially or fully obscured") + TangemLogger.d("Window is partially or fully obscured") if (!isWindowFullyObscuredAlreadySent) { analyticsEventHandler.send( diff --git a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt index 29b584fa05..62505009a4 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/AnalyticsEventsLogger.kt @@ -1,9 +1,9 @@ package com.tangem.tap.common.analytics import com.tangem.common.json.MoshiJsonConverter -import com.tangem.core.analytics.api.ExceptionLogger import com.tangem.core.analytics.api.EventLogger -import timber.log.Timber +import com.tangem.core.analytics.api.ExceptionLogger +import com.tangem.utils.logging.TangemLogger class AnalyticsEventsLogger( private val name: String, @@ -11,11 +11,11 @@ class AnalyticsEventsLogger( ) : EventLogger, ExceptionLogger { override fun logEvent(event: String, params: Map) { - Timber.d(jsonConverter.prettyPrint(PrintEventModel(name, event, params))) + TangemLogger.d(jsonConverter.prettyPrint(PrintEventModel(name, event, params))) } override fun logException(error: Throwable, params: Map) { - Timber.e(error, jsonConverter.prettyPrint(PrintEventModel(name, "error", params))) + TangemLogger.e(jsonConverter.prettyPrint(PrintEventModel(name, "error", params)), error) } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListener.kt b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListener.kt index ae2547a4a9..b43b358a5c 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListener.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerDeepLinkListener.kt @@ -2,7 +2,7 @@ package com.tangem.tap.common.analytics.appsflyer import com.appsflyer.deeplink.DeepLinkListener import com.appsflyer.deeplink.DeepLinkResult -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import javax.inject.Singleton @@ -17,10 +17,10 @@ class AppsFlyerDeepLinkListener @Inject constructor( referralParamsHandler.handle(deepLink = p0.deepLink) } DeepLinkResult.Status.NOT_FOUND -> { - Timber.i("No deep link found") + TangemLogger.i("No deep link found") } DeepLinkResult.Status.ERROR -> { - Timber.e("Deep link error: ${p0.error}") + TangemLogger.e("Deep link error: ${p0.error}") } } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt index a643b0142e..51352ef3ee 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt @@ -2,13 +2,13 @@ package com.tangem.tap.common.analytics.appsflyer import com.appsflyer.deeplink.DeepLink import com.tangem.datasource.local.appsflyer.AppsFlyerStore -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.wallets.models.AppsFlyerConversionData import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase +import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton import kotlin.contracts.ExperimentalContracts @@ -41,15 +41,15 @@ class AppsFlyerReferralParamsHandler @Inject constructor( private fun handle(deepLinkValue: String?, deepLinkSub1: String?, deepLinkSub2: String?) { if (deepLinkValue != REFERRAL_DEEP_LINK_VALUE) { - Timber.i("Ignoring deep link with value: ${deepLinkValue ?: "null"}") + TangemLogger.i("Ignoring deep link with value: ${deepLinkValue ?: "null"}") return } @Suppress("NullableToStringCall") - Timber.i("refcode=$deepLinkSub1\ncampaign=$deepLinkSub2") + TangemLogger.i("refcode=$deepLinkSub1\ncampaign=$deepLinkSub2") if (!isValidParam(deepLinkSub1)) { - Timber.e("Deeplink conversion data is invalid") + TangemLogger.e("Deeplink conversion data is invalid") return } @@ -69,7 +69,7 @@ class AppsFlyerReferralParamsHandler @Inject constructor( coroutineScope.launch { mutex.withLock { setShouldShowMobileWalletPromoUseCase(true) - .onLeft { Timber.e(it) } + .onLeft { TangemLogger.e("Error", it) } appsFlyerStore.storeIfAbsent( value = AppsFlyerConversionData(refcode = refcode, campaign = campaign), ) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/TangemAFConversionListener.kt b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/TangemAFConversionListener.kt index a051eefeb9..a0391029a5 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/TangemAFConversionListener.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/TangemAFConversionListener.kt @@ -1,7 +1,7 @@ package com.tangem.tap.common.analytics.appsflyer import com.appsflyer.AppsFlyerConversionListener -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import javax.inject.Singleton @@ -11,7 +11,7 @@ class TangemAFConversionListener @Inject constructor( ) : AppsFlyerConversionListener { override fun onConversionDataSuccess(p0: Map?) { - Timber.i("AppsFlyer conversion data success: ${p0.orEmpty()}") + TangemLogger.i("AppsFlyer conversion data success: ${p0.orEmpty()}") if (p0 == null) return @@ -19,14 +19,14 @@ class TangemAFConversionListener @Inject constructor( } override fun onConversionDataFail(p0: String?) { - Timber.e("AppsFlyer conversion data failure: ${p0.orEmpty()}") + TangemLogger.e("AppsFlyer conversion data failure: ${p0.orEmpty()}") } override fun onAppOpenAttribution(p0: Map?) { - Timber.i("AppsFlyer app open attribution: ${p0.orEmpty()}") + TangemLogger.i("AppsFlyer app open attribution: ${p0.orEmpty()}") } override fun onAttributionFailure(p0: String?) { - Timber.e("AppsFlyer attribution failure: ${p0.orEmpty()}") + TangemLogger.e("AppsFlyer attribution failure: ${p0.orEmpty()}") } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsClient.kt index 2f07e571b1..772c294d3b 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsClient.kt @@ -6,16 +6,16 @@ import com.appsflyer.attribution.AppsFlyerRequestListener import com.tangem.core.analytics.api.EventLogger import com.tangem.core.analytics.api.UserIdHolder import com.tangem.datasource.local.appsflyer.AppsFlyerStore -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.tap.common.analytics.appsflyer.AppsFlyerDeepLinkListener import com.tangem.tap.common.analytics.appsflyer.TangemAFConversionListener import com.tangem.tap.common.analytics.handlers.firebase.UnderscoreAnalyticsEventConverter +import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.launch -import timber.log.Timber interface AppsFlyerAnalyticsClient : EventLogger, UserIdHolder @@ -40,9 +40,9 @@ class AppsFlyerClient @AssistedInject constructor( init(apiKey, tangemAFConversionListener, context) - Timber.i("Starting AppsFlyer SDK") + TangemLogger.i("Starting AppsFlyer SDK") start(context, apiKey, InitializationListener) - Timber.i("AppsFlyer SDK started") + TangemLogger.i("AppsFlyer SDK started") saveUID() } @@ -57,7 +57,7 @@ class AppsFlyerClient @AssistedInject constructor( } override fun logEvent(event: String, params: Map) { - Timber.tag("AppsFlyer").i("Logging event: $event with params: $params") + TangemLogger.withTag("AppsFlyer").i("Logging event: $event with params: $params") appsFlyerLib.logEvent( context, event, @@ -78,21 +78,21 @@ class AppsFlyerClient @AssistedInject constructor( private object InitializationListener : AppsFlyerRequestListener { override fun onSuccess() { - Timber.d("AppsFlyer initialized successfully") + TangemLogger.d("AppsFlyer initialized successfully") } override fun onError(p0: Int, p1: String) { - Timber.e("AppsFlyer initialization error: $p0, $p1") + TangemLogger.e("AppsFlyer initialization error: $p0, $p1") } } private object LogEventListener : AppsFlyerRequestListener { override fun onSuccess() { - Timber.tag("AppsFlyerClient").i("AppsFlyerRequestListener send") + TangemLogger.withTag("AppsFlyerClient").i("AppsFlyerRequestListener send") } override fun onError(p0: Int, p1: String) { - Timber.tag("AppsFlyerClient").e("AppsFlyerRequestListener onError: $p0, $p1") + TangemLogger.withTag("AppsFlyerClient").e("AppsFlyerRequestListener onError: $p0, $p1") } } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsHandler.kt index 40dfd9b37a..611ca30dba 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/appsflyer/AppsFlyerAnalyticsHandler.kt @@ -6,14 +6,14 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AppsFlyerIncludedEvent import com.tangem.core.analytics.models.AppsFlyerOnlyEvent import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger class AppsFlyerAnalyticsHandler( private val client: AppsFlyerAnalyticsClient, ) : AnalyticsHandler, AnalyticsUserIdHandler { init { - Timber.tag("AppsFlyer").i("AppsFlyer Analytics Handler created") + TangemLogger.withTag("AppsFlyer").i("AppsFlyer Analytics Handler created") } override fun id(): String = ID @@ -21,11 +21,15 @@ class AppsFlyerAnalyticsHandler( override fun send(event: AnalyticsEvent) { when (event) { is AppsFlyerOnlyEvent -> { - Timber.tag("AppsFlyer").i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}") + TangemLogger.withTag( + "AppsFlyer", + ).i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}") client.logEvent(event.id, event.params) } is AppsFlyerIncludedEvent -> { - Timber.tag("AppsFlyer").i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}") + TangemLogger.withTag( + "AppsFlyer", + ).i("Sending event to AppsFlyer: ${event.id} with params: ${event.params}") val replacedEvent = event.appsFlyerReplacedEvent ?: event.event client.logEvent( event = AnalyticsEvent(category = event.category, event = replacedEvent).id, @@ -52,15 +56,15 @@ class AppsFlyerAnalyticsHandler( ) : AnalyticsHandlerBuilder { init { - Timber.tag("AppsFlyer").i("AppsFlyer Analytics Handler Builder created") + TangemLogger.withTag("AppsFlyer").i("AppsFlyer Analytics Handler Builder created") } override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler = AppsFlyerAnalyticsHandler( client = if (data.logConfig.isAppsflyerLogEnabled) { - Timber.tag("AppsFlyer").i("AppsFlyer log enabled, mock client created") + TangemLogger.withTag("AppsFlyer").i("AppsFlyer log enabled, mock client created") AppsFlyerLogClient(data.jsonConverter) } else { - Timber.tag("AppsFlyer").i("AppsFlyer log disabled, real client created") + TangemLogger.withTag("AppsFlyer").i("AppsFlyer log disabled, real client created") appsFlyerClientFactory.create(apiKey = data.config.appsFlyerApiKey) }, ) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt index ddd534e938..098d41abf3 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt @@ -1,11 +1,11 @@ package com.tangem.tap.common.analytics.handlers.customerio import android.app.Application +import com.tangem.utils.logging.TangemLogger import io.customer.messagingpush.ModuleMessagingPushFCM import io.customer.sdk.CustomerIO import io.customer.sdk.CustomerIOBuilder import io.customer.sdk.data.model.Region -import timber.log.Timber /** * Real Customer.io SDK client. @@ -32,7 +32,7 @@ internal class CustomerIoClient( .addCustomerIOModule(ModuleMessagingPushFCM()) .build() - Timber.d("CustomerIO SDK initialized") + TangemLogger.d("CustomerIO SDK initialized") } override fun setUserId(userId: String) { diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt index 62a36d09f1..6773cce899 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt @@ -1,6 +1,6 @@ package com.tangem.tap.common.analytics.handlers.customerio -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Log client for Customer.io (used in debug mode). @@ -13,11 +13,11 @@ internal class CustomerIoLogClient : CustomerIoAnalyticsClient { override fun setUserId(userId: String) { this.userId = userId - Timber.tag(CustomerIoAnalyticsHandler.ID).d("identify: userId=$userId") + TangemLogger.withTag(CustomerIoAnalyticsHandler.ID).d("identify: userId=$userId") } override fun clearUserId() { - Timber.tag(CustomerIoAnalyticsHandler.ID).d("clearIdentify: previous userId=$userId") + TangemLogger.withTag(CustomerIoAnalyticsHandler.ID).d("clearIdentify: previous userId=$userId") this.userId = null } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt index 5a8a85c072..a7117928b4 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAppInstanceIdProvider.kt @@ -3,8 +3,8 @@ package com.tangem.tap.common.analytics.handlers.firebase import com.google.firebase.analytics.ktx.analytics import com.google.firebase.ktx.Firebase import com.tangem.core.analytics.AppInstanceIdProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.suspendCancellableCoroutine -import timber.log.Timber import kotlin.coroutines.resume internal class FirebaseAppInstanceIdProvider : AppInstanceIdProvider { @@ -13,7 +13,7 @@ internal class FirebaseAppInstanceIdProvider : AppInstanceIdProvider { Firebase.analytics.appInstanceId .addOnSuccessListener { continuation.resume(it) } .addOnFailureListener { - Timber.w("Fail to get appInstanceId") + TangemLogger.w("Fail to get appInstanceId") continuation.resume(null) } } @@ -22,7 +22,7 @@ internal class FirebaseAppInstanceIdProvider : AppInstanceIdProvider { return try { Firebase.analytics.appInstanceId.result } catch (e: IllegalStateException) { - Timber.e(e, "getAppInstanceIdSync") + TangemLogger.e("getAppInstanceIdSync", e) null } } diff --git a/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt b/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt index 1c33fe0909..478f23edeb 100644 --- a/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt +++ b/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt @@ -6,7 +6,7 @@ import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN import android.os.Build import android.os.PersistableBundle import com.tangem.core.ui.clipboard.ClipboardManager -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import android.content.ClipboardManager as AndroidClipboardManager internal class DefaultClipboardManager(private val clipboardManager: AndroidClipboardManager) : ClipboardManager { @@ -24,13 +24,13 @@ internal class DefaultClipboardManager(private val clipboardManager: AndroidClip val clip = clipboardManager.primaryClip if (clip == null || clip.itemCount == 0) { - Timber.d("Clipboard is empty") + TangemLogger.d("Clipboard is empty") return default } val clipDescription = clipboardManager.primaryClipDescription if (clipDescription?.hasMimeType(MIMETYPE_TEXT_PLAIN) == false) { - Timber.d("Clipboard doesn't contain text") + TangemLogger.d("Clipboard doesn't contain text") return default } diff --git a/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt b/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt index 8013aa18f1..70d32a7166 100644 --- a/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt +++ b/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt @@ -1,12 +1,12 @@ package com.tangem.tap.common.clipboard import com.tangem.core.ui.clipboard.ClipboardManager -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal object MockClipboardManager : ClipboardManager { override fun setText(text: String, isSensitive: Boolean, label: String) { - Timber.w("Clipboard Manager not available") + TangemLogger.w("Clipboard Manager not available") } override fun getText(default: String?): String? = null diff --git a/app/src/main/java/com/tangem/tap/common/deeplink/DefaultDeeplinkLauncher.kt b/app/src/main/java/com/tangem/tap/common/deeplink/DefaultDeeplinkLauncher.kt index 3c6fa7e0c9..ce74ae1064 100644 --- a/app/src/main/java/com/tangem/tap/common/deeplink/DefaultDeeplinkLauncher.kt +++ b/app/src/main/java/com/tangem/tap/common/deeplink/DefaultDeeplinkLauncher.kt @@ -3,11 +3,11 @@ package com.tangem.tap.common.deeplink import android.content.Context import android.content.Intent import android.net.Uri +import androidx.core.net.toUri import com.tangem.common.routing.DeepLinkScheme import com.tangem.core.navigation.deeplink.DeeplinkLauncher import com.tangem.core.navigation.url.UrlOpener -import androidx.core.net.toUri -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * [DeeplinkLauncher] implementation that launches deep links as intents to the current Activity @@ -26,7 +26,7 @@ internal class DefaultDeeplinkLauncher( -> launchDeepLink(deeplinkUri) DeepLinkScheme.Https.scheme -> launchDeeplinkOrOpenBrowser(deeplinkUri, link) else -> { - Timber.i( + TangemLogger.i( """ No match found for deep link |- Received URI: $deeplinkUri diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index 3aeeb7d9cd..06dd58d367 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -10,8 +10,8 @@ import com.tangem.tap.domain.TapError import com.tangem.tap.domain.getFirstToken import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.delay -import timber.log.Timber /** [REDACTED_AUTHOR] @@ -31,7 +31,7 @@ suspend fun WalletManager.safeUpdate(isDemoCard: Boolean): Result = try Result.Success(wallet) } } catch (exception: Exception) { - Timber.e(exception) + TangemLogger.e("Error", exception) val networkConnectionManager = store.inject(DaggerGraphState::networkConnectionManager) if (!networkConnectionManager.isOnline) { diff --git a/app/src/main/java/com/tangem/tap/common/images/Coil.kt b/app/src/main/java/com/tangem/tap/common/images/Coil.kt index 39b02987d6..083711a2ca 100644 --- a/app/src/main/java/com/tangem/tap/common/images/Coil.kt +++ b/app/src/main/java/com/tangem/tap/common/images/Coil.kt @@ -9,10 +9,10 @@ import coil.decode.ImageDecoderDecoder import coil.decode.SvgDecoder import coil.memory.MemoryCache import coil.request.CachePolicy -import coil.util.Logger import com.tangem.datasource.api.common.createNetworkLoggingInterceptor +import com.tangem.utils.logging.TangemLogger import okhttp3.OkHttpClient -import timber.log.Timber +import coil.util.Logger as CoilLogger private const val COIL_LOG_TAG = "COIL" private const val COIL_MEMORY_CACHE_SIZE = 0.25 @@ -22,7 +22,7 @@ fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageL .apply { if (!logEnabled) return@apply - logger(CoilTimberLogger()) + logger(CoilKermitLogger()) okHttpClient { OkHttpClient.Builder() .addNetworkInterceptor(createNetworkLoggingInterceptor()) @@ -48,14 +48,16 @@ fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageL .build() } -private class CoilTimberLogger : Logger { +private class CoilKermitLogger : CoilLogger { override var level: Int = Log.DEBUG + private val logger = TangemLogger.withTag(COIL_LOG_TAG) override fun log(tag: String, priority: Int, message: String?, throwable: Throwable?) { - with(Timber.tag(COIL_LOG_TAG)) { - if (throwable != null) e(throwable, message) - if (message != null) d(message) + if (throwable != null) { + logger.e(message ?: "", throwable) + } else if (message != null) { + logger.d(message) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt b/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt index 8eaafd7505..0231c5fb23 100644 --- a/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt +++ b/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt @@ -8,8 +8,8 @@ import co.touchlab.kermit.Logger import co.touchlab.kermit.Severity import com.orhanobut.logger.AndroidLogAdapter import com.tangem.datasource.local.logs.AppLogsStore +import com.tangem.utils.logging.TangemLogger import com.tangem.wallet.BuildConfig -import timber.log.Timber import java.util.regex.Pattern import com.orhanobut.logger.Logger as PrettyLogger @@ -30,18 +30,9 @@ class TangemAppLoggerInitializer( PrettyLogger.addLogAdapter(AndroidLogAdapter(TimberFormatStrategy())) } - Timber.plant(tree = createTimberTree()) Logger.setLogWriters(KermitLogWriter(::finalLogOutput)) } - private fun createTimberTree(): Timber.Tree { - return object : Timber.DebugTree() { - override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { - finalLogOutput(priority = priority, tag = tag, message = message, t = t) - } - } - } - private fun finalLogOutput(priority: Int, tag: String?, message: String, t: Throwable?) { if (IS_LOG_ENABLED) { PrettyLogger.log(priority, tag, message, t) @@ -71,6 +62,8 @@ private class KermitLogWriter( KermitLogWriter::class.java.name, BaseLogger::class.java.name, Logger::class.java.name, + TangemLogger::class.java.name, + TangemLogger.TaggedLogger::class.java.name, ) override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) { @@ -87,7 +80,7 @@ private class KermitLogWriter( tag } else { /** - * like in [Timber.DebugTree.tag] + * like in [Logger.debugTree.tag] */ @Suppress("UnnecessaryLet", "ThrowingExceptionsWithoutMessageOrCause") Throwable().stackTrace @@ -99,7 +92,7 @@ private class KermitLogWriter( } /** - * copy from [Timber.DebugTree.createStackElementTag] + * copy from [Logger.debugTree.createStackElementTag] */ @Suppress("MagicNumber") private fun createStackElementTag(element: StackTraceElement): String? { @@ -120,7 +113,7 @@ private class KermitLogWriter( private const val KERMIT_LOGGER_DEFAULT_TAG = "" /** - * copy from [Timber.DebugTree.Companion] + * copy from [Logger.debugTree.Companion] */ private const val MAX_TAG_LENGTH = 23 private val ANONYMOUS_CLASS = Pattern.compile("(\\$\\d+)+$") diff --git a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt index 9965c9a2f2..ba6b3ddcfe 100644 --- a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt +++ b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt @@ -4,9 +4,9 @@ import android.annotation.SuppressLint import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import com.tangem.tap.common.analytics.CustomerIoFeatureToggles +import com.tangem.utils.logging.TangemLogger import dagger.hilt.android.AndroidEntryPoint import io.customer.messagingpush.CustomerIOFirebaseMessagingService -import timber.log.Timber import javax.inject.Inject @AndroidEntryPoint @@ -22,7 +22,7 @@ internal class TangemPushNotificationService : FirebaseMessagingService() { override fun onNewToken(token: String) { super.onNewToken(token) - Timber.d("New FCM token received: $token") + TangemLogger.d("New FCM token received: $token") if (customerIoFeatureToggles.isFeatureEnabled) { CustomerIOFirebaseMessagingService.onNewToken(applicationContext, token) diff --git a/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt index a544190398..7dd3992c54 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/LogMiddleware.kt @@ -1,7 +1,7 @@ package com.tangem.tap.common.redux +import com.tangem.utils.logging.TangemLogger import org.rekotlin.Middleware -import timber.log.Timber /** [REDACTED_AUTHOR] @@ -9,7 +9,7 @@ import timber.log.Timber val logMiddleware: Middleware = { _, _ -> { nextDispatch -> { action -> - Timber.i("Dispatch action: ${action::class.java.simpleName}") + TangemLogger.i("Dispatch action: ${action::class.java.simpleName}") nextDispatch(action) } } diff --git a/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt index e27db731ed..74e7e95205 100644 --- a/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt +++ b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt @@ -14,8 +14,8 @@ import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.getColorCompat import com.tangem.tap.foregroundActivityObserver import com.tangem.tap.withForegroundActivity +import com.tangem.utils.logging.TangemLogger import com.tangem.wallet.R -import timber.log.Timber internal class CustomTabsUrlOpener : UrlOpener { @@ -55,7 +55,7 @@ internal class CustomTabsUrlOpener : UrlOpener { customTabsIntent.launchUrl(context, url.toUri()) } }.onFailure { - Timber.e(it) + TangemLogger.e("Error", it) } } diff --git a/app/src/main/java/com/tangem/tap/core/DefaultAppCoroutineScope.kt b/app/src/main/java/com/tangem/tap/core/DefaultAppCoroutineScope.kt index 190af9d4eb..242483b1b1 100644 --- a/app/src/main/java/com/tangem/tap/core/DefaultAppCoroutineScope.kt +++ b/app/src/main/java/com/tangem/tap/core/DefaultAppCoroutineScope.kt @@ -1,10 +1,10 @@ package com.tangem.tap.core -import co.touchlab.kermit.Logger import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.SupervisorJob @@ -28,7 +28,7 @@ internal class DefaultAppCoroutineScope @Inject constructor( } private fun logError(throwable: Throwable, coroutineName: String) { - Logger.withTag(tag).e( + TangemLogger.withTag(tag).e( messageString = "CoroutineName $coroutineName", throwable = throwable, ) diff --git a/app/src/main/java/com/tangem/tap/core/navigation/email/AndroidEmailSender.kt b/app/src/main/java/com/tangem/tap/core/navigation/email/AndroidEmailSender.kt index 12e69c91b8..e0aea6d0e0 100644 --- a/app/src/main/java/com/tangem/tap/core/navigation/email/AndroidEmailSender.kt +++ b/app/src/main/java/com/tangem/tap/core/navigation/email/AndroidEmailSender.kt @@ -8,7 +8,7 @@ import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import com.tangem.core.navigation.email.EmailSender import com.tangem.tap.foregroundActivityObserver -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Implementation of email sender for Android @@ -21,7 +21,7 @@ internal class AndroidEmailSender : EmailSender { val activity = foregroundActivityObserver.foregroundActivity if (activity == null) { - Timber.e("Foreground activity not found") + TangemLogger.e("Foreground activity not found") return } @@ -50,7 +50,7 @@ internal class AndroidEmailSender : EmailSender { ContextCompat.startActivity(activity, chooserIntent, null) } catch (ex: Exception) { - Timber.e("Failed to send email: $ex") + TangemLogger.e("Failed to send email: $ex") } } diff --git a/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt b/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt index 10a4807522..308322f8aa 100644 --- a/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt +++ b/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt @@ -2,7 +2,7 @@ package com.tangem.tap.core.security import com.dexprotector.rtc.RtcStatus import com.tangem.security.DeviceSecurityInfoProvider -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider { override val isRooted: Boolean @@ -16,7 +16,7 @@ internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider { return try { RtcStatus.getRtcStatus() } catch (e: Throwable) { - Timber.e(e) + TangemLogger.e("Error", e) null } } diff --git a/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt b/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt index 7bc5ae57c9..fdfcf8789e 100644 --- a/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt +++ b/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt @@ -2,7 +2,7 @@ package com.tangem.tap.data import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.datasource.local.logs.AppLogsStore -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * BlockchainSDK logger implementation @@ -16,7 +16,7 @@ internal class TangemBlockchainSDKLogger( ) : BlockchainSDKLogger { override fun log(level: BlockchainSDKLogger.Level, message: String) { - Timber.d(message) + TangemLogger.d(message) appLogsStore.saveLogMessage(tag = "BlockchainSDK_${level.name}", message) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt index 27f82f9b3b..f9b56d8d88 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -19,10 +19,10 @@ import com.tangem.datasource.local.visa.VisaOtpData import com.tangem.datasource.local.visa.hasSavedOTP import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.domain.visa.error.VisaActivationError import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository -import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.operations.GenerateOTPCommand import com.tangem.operations.attestation.AttestCardKeyCommand import com.tangem.operations.pins.SetUserCodeCommand @@ -31,11 +31,11 @@ import com.tangem.operations.sign.SignHashResponse import com.tangem.operations.wallet.CreateWalletTask import com.tangem.sdk.api.visa.VisaCardActivationResponse import com.tangem.sdk.api.visa.VisaCardActivationTaskMode +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.* -import timber.log.Timber import kotlin.coroutines.resume import kotlin.time.measureTimedValue @@ -94,7 +94,7 @@ class VisaCardActivationTask @AssistedInject constructor( } } } - Timber.i("VisaCardActivationTask all time: ${timedResult.duration}") + TangemLogger.i("VisaCardActivationTask all time: ${timedResult.duration}") return timedResult.value } @@ -109,11 +109,11 @@ class VisaCardActivationTask @AssistedInject constructor( } } } - Timber.i("AttestCardKeyCommand time: ${timedResult.duration}") + TangemLogger.i("AttestCardKeyCommand time: ${timedResult.duration}") return when (val result = timedResult.value) { is CompletionResult.Success -> { - Timber.i("AttestCardKeyCommand success") + TangemLogger.i("AttestCardKeyCommand success") processSignedAuthorizationChallenge( signedChallenge = challengeToSign.toSignedChallenge( signedChallenge = result.data.cardSignature.toHexString(), @@ -122,7 +122,7 @@ class VisaCardActivationTask @AssistedInject constructor( ) } is CompletionResult.Failure -> { - Timber.e("AttestCardKeyCommand failure ${result.error}") + TangemLogger.e("AttestCardKeyCommand failure ${result.error}") CompletionResult.Failure(result.error) } } @@ -206,15 +206,15 @@ class VisaCardActivationTask @AssistedInject constructor( } } - Timber.i("CreateWalletTask time: ${timedResult.duration}") + TangemLogger.i("CreateWalletTask time: ${timedResult.duration}") when (val result = timedResult.value) { is CompletionResult.Success -> { - Timber.i("CreateWalletTask success") + TangemLogger.i("CreateWalletTask success") CompletionResult.Success(Unit) } is CompletionResult.Failure -> { - Timber.e("CreateWalletTask failure ${result.error}") + TangemLogger.e("CreateWalletTask failure ${result.error}") CompletionResult.Failure(result.error) } } @@ -237,11 +237,11 @@ class VisaCardActivationTask @AssistedInject constructor( } } - Timber.i("GenerateOTPCommand time: ${timedResult.duration}") + TangemLogger.i("GenerateOTPCommand time: ${timedResult.duration}") return when (val result = timedResult.value) { is CompletionResult.Success -> { - Timber.i("GenerateOTPCommand success") + TangemLogger.i("GenerateOTPCommand success") otpStorage.saveOTP( cardId = cardId, data = VisaOtpData(result.data.rootOTP, result.data.rootOTPCounter), @@ -249,7 +249,7 @@ class VisaCardActivationTask @AssistedInject constructor( CompletionResult.Success(Unit) } is CompletionResult.Failure -> { - Timber.e("GenerateOTPCommand failure ${result.error}") + TangemLogger.e("GenerateOTPCommand failure ${result.error}") CompletionResult.Failure(result.error) } } @@ -278,11 +278,11 @@ class VisaCardActivationTask @AssistedInject constructor( } } - Timber.i("SignHashCommand time: ${timedResult.duration}") + TangemLogger.i("SignHashCommand time: ${timedResult.duration}") return when (val result = timedResult.value) { is CompletionResult.Success -> { - Timber.i("SignHashCommand success") + TangemLogger.i("SignHashCommand success") handleSignedData( dataToSign = dataToSign, response = result.data, @@ -290,7 +290,7 @@ class VisaCardActivationTask @AssistedInject constructor( ) } is CompletionResult.Failure -> { - Timber.e("SignHashCommand failure ${result.error}") + TangemLogger.e("SignHashCommand failure ${result.error}") CompletionResult.Failure(result.error) } } @@ -336,7 +336,7 @@ class VisaCardActivationTask @AssistedInject constructor( return CompletionResult.Success(Unit) } - Timber.i("Setting access code") + TangemLogger.i("Setting access code") val task = SetUserCodeCommand.changeAccessCode(mode.accessCode) @@ -348,15 +348,15 @@ class VisaCardActivationTask @AssistedInject constructor( } } - Timber.i("SetUserCodeCommand time: ${timedResult.duration}") + TangemLogger.i("SetUserCodeCommand time: ${timedResult.duration}") return when (val result = timedResult.value) { is CompletionResult.Success -> { - Timber.i("SetUserCodeCommand success") + TangemLogger.i("SetUserCodeCommand success") CompletionResult.Success(Unit) } is CompletionResult.Failure -> { - Timber.i("SetUserCodeCommand failure ${result.error}") + TangemLogger.i("SetUserCodeCommand failure ${result.error}") CompletionResult.Failure(result.error) } } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt index f4a2c32083..1eb51a4e27 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsSensitiveInformationRepository.kt @@ -14,9 +14,9 @@ import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository import com.tangem.tap.domain.userWalletList.utils.sensitiveInformation +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import timber.log.Timber import javax.crypto.spec.SecretKeySpec internal class DefaultUserWalletsSensitiveInformationRepository( @@ -123,7 +123,7 @@ internal class DefaultUserWalletsSensitiveInformationRepository( this@decodeToSensitiveInformation.decodeToString(throwOnInvalidSequence = true), ) } catch (e: CharacterCodingException) { - Timber.e(e, "Unable to decode sensitive information") + TangemLogger.e("Unable to decode sensitive information", e) null } diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt index 2e96094cf1..ce85e49171 100644 --- a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt +++ b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt @@ -10,19 +10,18 @@ import com.tangem.common.extensions.toHexString import com.tangem.core.error.ext.tangemError import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility -import com.tangem.domain.visa.model.VisaCardActivationStatus +import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.domain.visa.error.VisaActivationError import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.error.VisaCardScanError import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository -import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource import com.tangem.operations.attestation.AttestCardKeyCommand import com.tangem.operations.attestation.AttestCardKeyResponse import com.tangem.operations.attestation.AttestWalletKeyResponse import com.tangem.operations.attestation.AttestWalletKeyTask +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.suspendCancellableCoroutine -import timber.log.Timber import javax.inject.Inject import kotlin.coroutines.resume @@ -38,10 +37,10 @@ internal class VisaCardScanHandler @Inject constructor( ) suspend fun handleVisaCardScan(session: CardSession): CompletionResult { - Timber.i("Attempting to handle Visa card scan") + TangemLogger.i("Attempting to handle Visa card scan") val card = session.environment.card ?: run { - Timber.e("Card is null") + TangemLogger.e("Card is null") return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) } @@ -68,12 +67,12 @@ internal class VisaCardScanHandler @Inject constructor( } private suspend fun SessionContext.handleWalletAuthorization(): CompletionResult { - Timber.i("Started handling authorization using Visa wallet") + TangemLogger.i("Started handling authorization using Visa wallet") val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { - Timber.e("Failed to find extended public key while handling wallet authorization") + TangemLogger.e("Failed to find extended public key while handling wallet authorization") return CompletionResult.Failure(VisaCardScanError.FailedToFindWallet.tangemError) } @@ -82,14 +81,14 @@ internal class VisaCardScanHandler @Inject constructor( return CompletionResult.Failure(it.tangemError) } - Timber.i("Requesting challenge for wallet authorization") + TangemLogger.i("Requesting challenge for wallet authorization") val challengeResponse = visaAuthRemoteDataSource.getCardWalletAuthChallenge( cardId = card.cardId, // This is the wallet public key, not the address and it's alright, as the API expects it in this format cardWalletAddress = wallet.publicKey.toHexString(), ).getOrElse { error -> - Timber.i("Failed to get Access token for Wallet public key authorization") + TangemLogger.i("Failed to get Access token for Wallet public key authorization") return CompletionResult.Failure(error.tangemError) } @@ -103,7 +102,7 @@ internal class VisaCardScanHandler @Inject constructor( val signature = signChallengeResult.data.walletSignature val salt = signChallengeResult.data.salt - Timber.i("Challenge signed with Wallet public key") + TangemLogger.i("Challenge signed with Wallet public key") handleWalletAuthorizationTokens( cardWalletAddress = walletAddress.value, signedChallenge = challengeResponse.toSignedChallenge( @@ -113,7 +112,9 @@ internal class VisaCardScanHandler @Inject constructor( ) } is CompletionResult.Failure -> { - Timber.e("Error during Wallet authorization process. Tangem Sdk Error: ${signChallengeResult.error}") + TangemLogger.e( + "Error during Wallet authorization process. Tangem Sdk Error: ${signChallengeResult.error}", + ) CompletionResult.Failure(signChallengeResult.error) } } @@ -125,19 +126,19 @@ internal class VisaCardScanHandler @Inject constructor( ): CompletionResult { val authorizationTokensResponse = visaAuthRemoteDataSource.getAccessTokens(signedChallenge = signedChallenge) .getOrElse { error -> - Timber.i("Failed to get Access token for Wallet public key authorization.") + TangemLogger.i("Failed to get Access token for Wallet public key authorization.") return if ( error is VisaApiError.ProductInstanceIsNotActivated || error is VisaApiError.ProductInstanceNotFoundActivationRequired ) { - Timber.i("Proceeding with card authorization.") + TangemLogger.i("Proceeding with card authorization.") handleCardAuthorization(cardWalletAddress = cardWalletAddress) } else { CompletionResult.Failure(error.tangemError) } } - Timber.i("Authorized using Wallet public key successfully") + TangemLogger.i("Authorized using Wallet public key successfully") return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse)) } @@ -148,27 +149,27 @@ internal class VisaCardScanHandler @Inject constructor( ): CompletionResult { val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - Timber.i("Requesting authorization challenge to sign") + TangemLogger.i("Requesting authorization challenge to sign") val challengeResponse = visaAuthRemoteDataSource.getCardAuthChallenge( cardId = card.cardId, cardPublicKey = card.cardPublicKey.toHexString(), ).getOrElse { error -> - Timber.e("Failed to get challenge for Card authorization. Plain error: ${error.errorCode}") + TangemLogger.e("Failed to get challenge for Card authorization. Plain error: ${error.errorCode}") return CompletionResult.Failure(error.tangemError) } - Timber.i("Received challenge to sign: ${challengeResponse.challenge}") + TangemLogger.i("Received challenge to sign: ${challengeResponse.challenge}") val signChallengeResult = signChallengeWithCard(challenge = challengeResponse.challenge) val attestCardKeyResponse = when (signChallengeResult) { is CompletionResult.Success -> { - Timber.i("Challenged signed.") + TangemLogger.i("Challenged signed.") signChallengeResult.data } is CompletionResult.Failure -> { - Timber.e( + TangemLogger.e( "Failed to sign challenge with Card public key. Tangem Sdk Error: ${signChallengeResult.error}", ) return CompletionResult.Failure(signChallengeResult.error) @@ -181,7 +182,7 @@ internal class VisaCardScanHandler @Inject constructor( salt = attestCardKeyResponse.salt.toHexString(), ), ).getOrElse { error -> - Timber.e("Failed to sign challenge with Card public key. Plain error: ${error.errorCode}") + TangemLogger.e("Failed to sign challenge with Card public key. Plain error: ${error.errorCode}") return CompletionResult.Failure(error.tangemError) } @@ -191,7 +192,7 @@ internal class VisaCardScanHandler @Inject constructor( ) val activationRemoteState = visaActivationRepository.getActivationRemoteState().getOrElse { error -> - Timber.e("Failed to sign challenge with Card public key. Plain error: ${error.errorCode}") + TangemLogger.e("Failed to sign challenge with Card public key. Plain error: ${error.errorCode}") return CompletionResult.Failure(error.tangemError) } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 6ecd467e40..724134783f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -20,6 +20,7 @@ import com.tangem.tap.store import com.tangem.tap.tangemSdkManager import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged @@ -29,7 +30,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.rekotlin.Action import org.rekotlin.Middleware -import timber.log.Timber @Suppress("MemberNameEqualsClassName") class DetailsMiddleware { @@ -228,7 +228,7 @@ class DetailsMiddleware { ) } .doOnFailure { error -> - Timber.e(error, "Unable to delete saved access codes") + TangemLogger.e("Unable to delete saved access codes", error) } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt index 22e94ae7d3..b395cc0bf5 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt @@ -36,11 +36,11 @@ import com.tangem.tap.features.details.ui.common.utils.* import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addIf +import com.tangem.utils.logging.TangemLogger import com.tangem.wallet.R import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -244,7 +244,7 @@ internal class CardSettingsModel @Inject constructor( when (val result = tangemSdkManager.setAccessCode(scanResponse.card.cardId)) { is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged()) is CompletionResult.Failure -> { - Timber.e("Failed to change access code: ${result.error}") + TangemLogger.e("Failed to change access code: ${result.error}") } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt index 6e124933fc..10ec357e6b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/model/ResetCardModel.kt @@ -28,6 +28,7 @@ import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE +import com.tangem.utils.logging.TangemLogger import com.tangem.wallet.R import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -35,7 +36,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -190,7 +190,7 @@ internal class ResetCardModel @Inject constructor( resetCardUseCase(cardId = primaryCardId, params = currentUserCodeParams).onRight { deleteSavedAccessCodesUseCase(cardId = primaryCardId) val hasUserWallets = deleteWalletUseCase(userWalletId = currentUserWalletId).getOrElse { error -> - Timber.e("Unable to delete user wallet: $error") + TangemLogger.e("Unable to delete user wallet: $error") return@launch } 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 b8fd319359..be57cd0aa2 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 @@ -43,13 +43,13 @@ import com.tangem.tap.network.exchangeServices.SellService import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import com.tangem.wallet.BuildConfig import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout -import timber.log.Timber import javax.inject.Inject import kotlin.time.Duration.Companion.seconds @@ -155,7 +155,7 @@ internal class MainViewModel @Inject constructor( private suspend fun fetchUserCountry() { fetchUserCountryUseCase().onLeft { - Timber.e("Unable to fetch the user country code $it") + TangemLogger.e("Unable to fetch the user country code $it") } } @@ -189,8 +189,8 @@ internal class MainViewModel @Inject constructor( private suspend fun fetchStakingOptions() { fetchStakingOptionsUseCase() - .onLeft { Timber.e(it.toString(), "Unable to fetch staking options") } - .onRight { Timber.d("Staking options were fetched successfully") } + .onLeft { TangemLogger.e("Unable to fetch staking options: $it") } + .onRight { TangemLogger.d("Staking options were fetched successfully") } } private fun initializeOffRamp() { @@ -346,7 +346,9 @@ internal class MainViewModel @Inject constructor( val keyboardId = keyboardValidator.getKeyboardId() if (keyboardId != null) { - Timber.d("Keyboard ID: https://play.google.com/store/apps/details?id=${keyboardId.getPackageName()}") + TangemLogger.d( + "Keyboard ID: https://play.google.com/store/apps/details?id=${keyboardId.getPackageName()}", + ) analyticsEventHandler.send( event = TechAnalyticsEvent.KeyboardIdentifier( @@ -356,7 +358,7 @@ internal class MainViewModel @Inject constructor( ), ) } else { - Timber.e("Unable to get keyboard identifier") + TangemLogger.e("Unable to get keyboard identifier") } } } @@ -370,7 +372,7 @@ internal class MainViewModel @Inject constructor( refresh = true, ).fold( ifLeft = { error -> - Timber.e(error) + TangemLogger.e("Error", error) analyticsEventHandler.send( StoriesEvents.Error( type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType, @@ -387,7 +389,7 @@ internal class MainViewModel @Inject constructor( ) } } catch (ex: Exception) { - Timber.e(ex) + TangemLogger.e("Error", ex) analyticsEventHandler.send( StoriesEvents.Error( type = StoryContentIds.STORY_FIRST_TIME_SWAP.analyticType, @@ -412,7 +414,7 @@ internal class MainViewModel @Inject constructor( associateAndUpdateWallets(applicationId = applicationId) } } - .onLeft(Timber::e) + .onLeft { TangemLogger.e("Error", it) } } private suspend fun associateAndUpdateWallets(applicationId: ApplicationId) { 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 742e44ec5e..8807a899e6 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -19,9 +19,9 @@ import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.SellService import com.tangem.tap.network.exchangeServices.SellServiceInitializationStatus import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import timber.log.Timber import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec @@ -42,13 +42,13 @@ class MoonPayService( override suspend fun update() { withIOContext { - Timber.i("Start updating") + TangemLogger.i("Start updating") _initializationStatus.value = lceLoading() performRequest { val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) { is Result.Failure -> { - Timber.e(result.error, "Failed to load user status") + TangemLogger.e("Failed to load user status", result.error) _initializationStatus.value = result.error.lceError() return@performRequest } @@ -57,7 +57,7 @@ class MoonPayService( val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) { is Result.Failure -> { - Timber.e(result.error, "Failed to load currencies") + TangemLogger.e("Failed to load currencies", result.error) _initializationStatus.value = result.error.lceError() return@performRequest } @@ -77,7 +77,7 @@ class MoonPayService( ) } - Timber.i("Successfully updated") + TangemLogger.i("Successfully updated") _initializationStatus.value = lceContent() status = MoonPayStatus(currenciesToSell, userStatus, currencies) } diff --git a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt index ad66a52dce..d9cd7759f2 100644 --- a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt +++ b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt @@ -7,10 +7,10 @@ import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.decompose.navigation.Router import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import com.tangem.wallet.R import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber import kotlin.reflect.KClass internal class ProxyAppRouter( @@ -51,7 +51,7 @@ internal class ProxyAppRouter( runCatching { innerRouter.replaceAll(*routes, onComplete = onComplete) }.getOrElse { - Timber.e(it) + TangemLogger.e("Error", it) } } } @@ -76,12 +76,12 @@ internal class ProxyAppRouter( private fun safeNavigate(onComplete: (isSuccess: Boolean) -> Unit, message: String, block: () -> Unit) { routerScope.launch(dispatchers.mainImmediate) { - Timber.i(message) + TangemLogger.i(message) try { block() } catch (e: Throwable) { - Timber.e(e) + TangemLogger.e("Error", e) onComplete(false) } } @@ -90,7 +90,7 @@ internal class ProxyAppRouter( override fun defaultCompletionHandler(isSuccess: Boolean, errorMessage: String) { if (!isSuccess) { analyticsExceptionHandler.sendException(ExceptionAnalyticsEvent(RuntimeException(errorMessage))) - Timber.w(errorMessage) + TangemLogger.w(errorMessage) with(receiver = config.snackbarHandler ?: return) { showSnackbar( diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index 301a49d9ba..a4cdba0883 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -45,11 +45,11 @@ import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.utils.ChildFactory import com.tangem.tap.routing.utils.DeepLinkFactory import com.tangem.tap.store +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.launch -import timber.log.Timber @Suppress("LongParameterList") internal class DefaultRoutingComponent @AssistedInject constructor( @@ -101,7 +101,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( try { childFactory.createChild(route, childByContext(childContext)) } catch (e: Exception) { - Timber.e(e, "App Router Failed") + TangemLogger.e("App Router Failed", e) analyticsExceptionHandler.sendException( ExceptionAnalyticsEvent(exception = e, params = mapOf("Category" to "App Routing")), ) diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index bf888af247..fb1acbcd80 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -23,6 +23,7 @@ import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLi import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import com.tangem.utils.extensions.uriValidate +import com.tangem.utils.logging.TangemLogger import dagger.hilt.android.scopes.ActivityScoped import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -30,7 +31,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.transformLatest -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -62,7 +62,7 @@ internal class DeepLinkFactory @Inject constructor( fun handleDeeplink(deeplinkUri: Uri, coroutineScope: CoroutineScope, isFromOnNewIntent: Boolean) { lastDeepLink = deeplinkUri - Timber.i( + TangemLogger.i( """ Received deep link intent |- Received URI: $deeplinkUri @@ -108,7 +108,7 @@ internal class DeepLinkFactory @Inject constructor( DeepLinkScheme.Tangem.scheme -> handleTangemDeepLinks(deeplinkUri, coroutineScope, isFromOnNewIntent) DeepLinkScheme.WalletConnect.scheme -> walletConnectDeepLink.create(deeplinkUri) else -> { - Timber.i( + TangemLogger.i( """ No match found for deep link |- Received URI: $deeplinkUri @@ -157,7 +157,7 @@ internal class DeepLinkFactory @Inject constructor( DeepLinkRoute.Promo.host -> promoDeepLink.create(coroutineScope, queryParams) DeepLinkRoute.OnboardVisa.host -> onboardVisaDeepLink.create(deeplinkUri) else -> { - Timber.i( + TangemLogger.i( """ No match found for deep link |- Received URI: $deeplinkUri diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt index 1692f38f75..0cb5e4db8f 100644 --- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt @@ -29,7 +29,6 @@ import kotlinx.coroutines.test.* import org.junit.After import org.junit.Before import org.junit.Test -import timber.log.Timber @OptIn(ExperimentalCoroutinesApi::class) class DeepLinkFactoryTest { @@ -126,7 +125,6 @@ class DeepLinkFactoryTest { every { mockedUri.port } returns 443 // Default HTTPS port every { mockedUri.fragment } returns null // No fragment in this URI - Timber.uprootAll() // Disable Timber logging for tests } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 30cd9f7258..0e396106b3 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -25,7 +25,6 @@ dependencies { implementation(deps.firebase.messaging) // end - implementation(deps.timber) implementation(deps.arrow.core) diff --git a/common/routing/build.gradle.kts b/common/routing/build.gradle.kts index b34c827c74..253b68bc9c 100644 --- a/common/routing/build.gradle.kts +++ b/common/routing/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { /* Core */ implementation(projects.core.decompose) implementation(projects.core.configToggles) + implementation(projects.core.utils) /* Domain */ implementation(projects.domain.qrScanning.models) @@ -30,7 +31,6 @@ dependencies { /* Libs - Other */ api(deps.kotlin.serialization) implementation(deps.androidx.core.ktx) - implementation(deps.timber) /* Tests */ testImplementation(deps.test.junit) diff --git a/common/src/main/kotlin/com/tangem/common/uri/ExternalUrlValidator.kt b/common/src/main/kotlin/com/tangem/common/uri/ExternalUrlValidator.kt index 9f3973c29b..49785fb80e 100644 --- a/common/src/main/kotlin/com/tangem/common/uri/ExternalUrlValidator.kt +++ b/common/src/main/kotlin/com/tangem/common/uri/ExternalUrlValidator.kt @@ -1,7 +1,7 @@ package com.tangem.common.uri import com.google.firebase.crashlytics.FirebaseCrashlytics -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.net.URI /** @@ -22,7 +22,7 @@ object ExternalUrlValidator { } catch (e: Exception) { val exception = IllegalStateException("Failed to validate URI: $externalUri", e) - Timber.e(exception) + TangemLogger.e("Error", exception) FirebaseCrashlytics.getInstance().recordException(exception) false diff --git a/core/ab-tests/build.gradle.kts b/core/ab-tests/build.gradle.kts index d47ebe83f1..f557fcda9e 100644 --- a/core/ab-tests/build.gradle.kts +++ b/core/ab-tests/build.gradle.kts @@ -17,7 +17,6 @@ dependencies { kapt(deps.hilt.kapt) /** Other libraries */ - implementation(deps.timber) /** Core modules */ implementation(projects.core.analytics.models) diff --git a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt index 703c717a79..7869bdbc27 100644 --- a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt +++ b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt @@ -8,8 +8,8 @@ import com.amplitude.experiment.ExperimentUser import com.tangem.core.abtests.manager.ABTestsManager import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.launch -import timber.log.Timber internal class AmplitudeABTestsManager( val application: Application, @@ -21,7 +21,7 @@ internal class AmplitudeABTestsManager( override fun init() { if (::client.isInitialized) { - Timber.w("AB Tests manager already initialized, skipping") + TangemLogger.w("AB Tests manager already initialized, skipping") return } @@ -40,7 +40,7 @@ internal class AmplitudeABTestsManager( val allVariants = client.all() logAllVariants(allVariants) } catch (exception: Exception) { - Timber.e(exception, "Failed to fetch AB test variants") + TangemLogger.e("Failed to fetch AB test variants", exception) } } } @@ -69,23 +69,23 @@ internal class AmplitudeABTestsManager( } private fun logAllVariants(allVariants: Map) { - Timber.d("=".repeat(SEPARATOR_LENGTH)) - Timber.d("AB Tests: Fetched ${allVariants.size} variants") - Timber.d("=".repeat(SEPARATOR_LENGTH)) + TangemLogger.d("=".repeat(SEPARATOR_LENGTH)) + TangemLogger.d("AB Tests: Fetched ${allVariants.size} variants") + TangemLogger.d("=".repeat(SEPARATOR_LENGTH)) if (allVariants.isEmpty()) { - Timber.d("No variants available") + TangemLogger.d("No variants available") } else { allVariants.entries.forEachIndexed { index, (key, variant) -> - Timber.d("[${index + 1}/${allVariants.size}] Key: $key") - Timber.d(" → Value: ${variant.value ?: "null"}") - Timber.d(" → Payload: ${variant.payload ?: "null"}") - Timber.d(" → Key: ${variant.key ?: "null"}") - Timber.d("-".repeat(SEPARATOR_LENGTH)) + TangemLogger.d("[${index + 1}/${allVariants.size}] Key: $key") + TangemLogger.d(" → Value: ${variant.value ?: "null"}") + TangemLogger.d(" → Payload: ${variant.payload ?: "null"}") + TangemLogger.d(" → Key: ${variant.key ?: "null"}") + TangemLogger.d("-".repeat(SEPARATOR_LENGTH)) } } - Timber.d("=".repeat(SEPARATOR_LENGTH)) + TangemLogger.d("=".repeat(SEPARATOR_LENGTH)) } private companion object { diff --git a/core/config-toggles/build.gradle.kts b/core/config-toggles/build.gradle.kts index 4b01f4402d..55ba623cd7 100644 --- a/core/config-toggles/build.gradle.kts +++ b/core/config-toggles/build.gradle.kts @@ -73,7 +73,6 @@ dependencies { /** Other libraries */ implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) /** Core modules */ diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/Version.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/Version.kt index f19eb0ce0b..f5d01447c9 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/Version.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/version/Version.kt @@ -1,7 +1,7 @@ package com.tangem.core.configtoggle.version import androidx.annotation.VisibleForTesting -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Presentation of application version (..). @@ -67,7 +67,7 @@ internal class Version private constructor(value: String) : Comparable return try { Version(value) } catch (exception: Exception) { - Timber.e(exception, "Invalid version - %s", value) + TangemLogger.e("Invalid version - $value", exception) return null } } diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 353cb3f5ab..34a72ebb46 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -97,7 +97,6 @@ dependencies { implementation(deps.kotlin.datetime) /** Logging */ - implementation(deps.timber) /** Network */ implementation(deps.moshi) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt index 28c7fa4f72..82329e9e3c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt @@ -1,12 +1,12 @@ package com.tangem.datasource.api.common.response import com.tangem.core.analytics.api.AnalyticsErrorHandler +import com.tangem.utils.logging.TangemLogger import okhttp3.Request import okio.Timeout import retrofit2.Call import retrofit2.Callback import retrofit2.Response -import timber.log.Timber internal class ApiResponseCallDelegate( private val wrappedCall: Call, @@ -41,10 +41,10 @@ internal class ApiResponseCallDelegate( val error = try { t.toApiError() } catch (e: ApiResponseError) { - Timber.e(e, "error map toApiError") + TangemLogger.e("error map toApiError", e) e } catch (e: Exception) { - Timber.e(e, "onFailure UnknownException") + TangemLogger.e("onFailure UnknownException", e) ApiResponseError.UnknownException(e) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt index ec7f06e90d..e83dc434c0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt @@ -2,9 +2,9 @@ package com.tangem.datasource.api.common.response import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.datasource.api.common.response.analytics.ApiErrorEvent +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.TimeoutCancellationException import retrofit2.Response -import timber.log.Timber import java.net.ConnectException import java.net.SocketTimeoutException import java.net.UnknownHostException @@ -31,7 +31,7 @@ internal fun Response.toSafeApiResponse(analyticsErrorHandler: Anal ApiResponseError.HttpException(code, message(), errorBody) } } catch (e: Exception) { - Timber.e(e, "UnknownException occured") + TangemLogger.e("UnknownException occured", e) ApiResponseError.UnknownException(e) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt index 9325df9a70..3a8e5629f9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/asset/loader/AssetLoader.kt @@ -9,8 +9,8 @@ import com.tangem.datasource.asset.reader.AssetReader import com.tangem.datasource.di.NetworkMoshi import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -49,7 +49,10 @@ class AssetLoader @Inject constructor( json = json, ) - Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + TangemLogger.e( + "Error", + IllegalStateException("Parsed config [$fileName] is null"), + ) } parsedConfig @@ -61,51 +64,65 @@ class AssetLoader @Inject constructor( json = json, ) - Timber.e(throwable, "Failed to load config [$fileName] from assets") + TangemLogger.e("Failed to load config [$fileName] from assets", throwable) null }, ) } /** Load list [V] values of asset file [fileName] */ - suspend inline fun loadList(fileName: String): List = runCatching(dispatchers.io) { - val json = assetReader.read(fullFileName = "$fileName.json") + suspend inline fun loadList(fileName: String): List { + val result = runCatching(dispatchers.io) { + val json = assetReader.read(fullFileName = "$fileName.json") - val type = Types.newParameterizedType(List::class.java, V::class.java) - val adapter = moshi.adapter>(type) + val type = Types.newParameterizedType(List::class.java, V::class.java) + val adapter = moshi.adapter>(type) - adapter.fromJson(json) - } - .fold( + adapter.fromJson(json) + } + return result.fold( onSuccess = { parsedConfig -> - if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + if (parsedConfig == null) { + TangemLogger.e( + "Error", + IllegalStateException("Parsed config [$fileName] is null"), + ) + } parsedConfig.orEmpty() }, onFailure = { throwable -> - Timber.e(throwable, "Failed to load config [$fileName] from assets") + TangemLogger.e("Failed to load config [$fileName] from assets", throwable) emptyList() }, ) + } /** Load map [String] keys and [V] values of asset file [fileName] */ - suspend inline fun loadMap(fileName: String): Map = runCatching(dispatchers.io) { - val json = assetReader.read(fullFileName = "$fileName.json") + suspend inline fun loadMap(fileName: String): Map { + val result = runCatching(dispatchers.io) { + val json = assetReader.read(fullFileName = "$fileName.json") - val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) - val adapter = moshi.adapter>(type) + val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java) + val adapter = moshi.adapter>(type) - adapter.fromJson(json) - } - .fold( + adapter.fromJson(json) + } + return result.fold( onSuccess = { parsedConfig -> - if (parsedConfig == null) Timber.e(IllegalStateException("Parsed config [$fileName] is null")) + if (parsedConfig == null) { + TangemLogger.e( + "Error", + IllegalStateException("Parsed config [$fileName] is null"), + ) + } parsedConfig.orEmpty() }, onFailure = { throwable -> - Timber.e(throwable, "Failed to load config [$fileName] from assets") + TangemLogger.e("Failed to load config [$fileName] from assets", throwable) emptyMap() }, ) + } fun sendException(fileName: String, isParsingSuccess: Boolean, json: String?) { analyticsExceptionHandler.sendException( diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt index 4da4c8b17d..6516a08182 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt @@ -1,10 +1,10 @@ package com.tangem.datasource.di +import com.tangem.utils.logging.TangemLogger import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.Response import okhttp3.ResponseBody.Companion.toResponseBody -import timber.log.Timber class StatusCodeInterceptor : Interceptor { @@ -12,7 +12,7 @@ class StatusCodeInterceptor : Interceptor { val originalResponse = chain.proceed(chain.request()) if (shouldInterceptResponse(originalResponse)) { - Timber.e("StatusCodeInterceptor INTERCEPTED%s", originalResponse.request.url.toString()) + TangemLogger.e("StatusCodeInterceptor INTERCEPTED${originalResponse.request.url}") val body = getBody().toResponseBody("application/json".toMediaTypeOrNull()) val code = getCode() diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/appsflyer/DefaultAppsFlyerStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/appsflyer/DefaultAppsFlyerStore.kt index bc349dac1c..8226326f81 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/appsflyer/DefaultAppsFlyerStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/appsflyer/DefaultAppsFlyerStore.kt @@ -8,7 +8,7 @@ import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.domain.wallets.models.AppsFlyerConversionData -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultAppsFlyerStore( private val appPreferencesStore: AppPreferencesStore, @@ -18,14 +18,14 @@ internal class DefaultAppsFlyerStore( val dto = appPreferencesStore.getObjectSyncOrNull(CONVERSION_DATA_KEY) ?: return null return ConversionDataConverter.convertBack(value = dto).also { - Timber.i("Getting conversion data from store: $it") + TangemLogger.i("Getting conversion data from store: $it") } } override suspend fun getUID(): String? = appPreferencesStore.getSyncOrNull(UID_KEY) override suspend fun store(value: AppsFlyerConversionData) { - Timber.i("Storing conversion data to store: $value") + TangemLogger.i("Storing conversion data to store: $value") val dto = ConversionDataConverter.convert(value) @@ -33,24 +33,24 @@ internal class DefaultAppsFlyerStore( } override suspend fun storeIfAbsent(value: AppsFlyerConversionData) { - Timber.i("Storing conversion data to store if absent: $value") + TangemLogger.i("Storing conversion data to store if absent: $value") appPreferencesStore.editData { preferences -> val saved = preferences[CONVERSION_DATA_KEY] if (saved == null) { - Timber.i("Conversion data is absent, storing $value") + TangemLogger.i("Conversion data is absent, storing $value") preferences.setObject(CONVERSION_DATA_KEY, ConversionDataConverter.convert(value)) } } } override suspend fun storeUIDIfAbsent(value: String) { - Timber.i("Storing UID to store if absent: $value") + TangemLogger.i("Storing UID to store if absent: $value") appPreferencesStore.editData { preferences -> val saved = preferences[UID_KEY] if (saved == null) { - Timber.i("UID is absent, storing $value") + TangemLogger.i("UID is absent, storing $value") preferences[UID_KEY] = value } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt index 007f386f0e..65dd45cf46 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt @@ -3,13 +3,14 @@ package com.tangem.datasource.local.logs import android.content.Context import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.* +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext import org.joda.time.DateTime import org.joda.time.format.DateTimeFormatterBuilder -import timber.log.Timber import java.io.* import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream @@ -121,7 +122,7 @@ class AppLogsStore @Inject constructor( private fun createFileIfNotExist() { if (!logFile.exists()) { runCatching { logFile.createNewFile() } - .onFailure(Timber::e) + .onFailure { TangemLogger.e("Error", it) } } } @@ -129,7 +130,7 @@ class AppLogsStore @Inject constructor( scope.launch { mutex.withLock { runCatching { callback() } - .onFailure(Timber::e) + .onFailure { TangemLogger.e("Error", it) } } } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt index 72835aa183..db9df71d9d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt @@ -17,7 +17,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_ import com.tangem.datasource.local.preferences.utils.CleanupKeyMigration import com.tangem.datasource.local.preferences.utils.SharedPreferencesKeyMigration import com.tangem.utils.coroutines.AppCoroutineScope -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Application preferences data store 'DataStore'. @@ -49,7 +49,7 @@ internal object PreferencesDataStore { private fun createCorruptionHandler(): ReplaceFileCorruptionHandler { return ReplaceFileCorruptionHandler( produceNewData = { corruptionException -> - Timber.w(corruptionException) + TangemLogger.w("Error", corruptionException) emptyPreferences() }, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/WireMockRedirectInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/WireMockRedirectInterceptor.kt index b246a027cb..4451945383 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/WireMockRedirectInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/WireMockRedirectInterceptor.kt @@ -1,8 +1,8 @@ package com.tangem.datasource.utils +import com.tangem.utils.logging.TangemLogger import okhttp3.Interceptor import okhttp3.Response -import timber.log.Timber /** * OkHttp interceptor that redirects requests from wiremock.tests-d.com to a local WireMock instance. @@ -17,7 +17,7 @@ class WireMockRedirectInterceptor : Interceptor { if (url.contains(WIREMOCK_REMOTE_URL)) { val newUrl = url.replace(WIREMOCK_REMOTE_URL, override.trimEnd('/')) - Timber.d("WireMockRedirect: $url -> $newUrl") + TangemLogger.d("WireMockRedirect: $url -> $newUrl") val newRequest = request.newBuilder() .url(newUrl) .build() diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt index 5650f72e16..1b1693d5a7 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt @@ -4,13 +4,13 @@ import com.google.common.truth.Truth import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.utils.ProviderSuspend +import com.tangem.utils.logging.TangemLogger import io.mockk.clearMocks import io.mockk.every import io.mockk.mockk import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance -import timber.log.Timber /** [REDACTED_AUTHOR] @@ -38,7 +38,7 @@ class ApiConfigTest { // Actual val actual = allBaseUrls.all { it.endsWith("/") } - Timber.e(allBaseUrls.joinToString(separator = "\n")) + TangemLogger.e(allBaseUrls.joinToString(separator = "\n")) // Assert Truth.assertThat(actual).isTrue() diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts index 8dc7d5437d..3020e44ee4 100644 --- a/core/navigation/build.gradle.kts +++ b/core/navigation/build.gradle.kts @@ -11,10 +11,11 @@ android { } dependencies { + implementation(projects.core.utils) + implementation(deps.hilt.android) kapt(deps.hilt.kapt) implementation(deps.material) implementation(deps.reKotlin) - implementation(deps.timber) } \ No newline at end of file diff --git a/core/res/build.gradle.kts b/core/res/build.gradle.kts index c444a12b01..9fe72ddbb5 100644 --- a/core/res/build.gradle.kts +++ b/core/res/build.gradle.kts @@ -12,7 +12,6 @@ dependencies { implementation(projects.core.utils) - implementation(deps.timber) // region Firebase libraries implementation(platform(deps.firebase.bom)) diff --git a/core/res/src/main/java/com/tangem/core/res/Resources.kt b/core/res/src/main/java/com/tangem/core/res/Resources.kt index 3964c5c093..d795fd2f00 100644 --- a/core/res/src/main/java/com/tangem/core/res/Resources.kt +++ b/core/res/src/main/java/com/tangem/core/res/Resources.kt @@ -5,7 +5,7 @@ import androidx.annotation.PluralsRes import androidx.annotation.StringRes import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.utils.SupportedLanguages -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Get a string resource safely or the resource name if an exception is thrown @@ -87,7 +87,7 @@ private fun reportIssue(throwable: Throwable, resources: Resources, id: Int, var "\terror message: ${throwable.message.orEmpty()}\n", ) - Timber.tag("Resources").e(exception) + TangemLogger.withTag("Resources").e("Error", exception) FirebaseCrashlytics.getInstance().recordException(exception) } \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 9be1ed52f9..87b86b8135 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -59,7 +59,6 @@ dependencies { implementation(deps.kotlin.immutable.collections) implementation(deps.zxing.qrCore) api(deps.jodatime) - implementation(deps.timber) implementation(deps.markdown) api(deps.haze) { exclude(module = "activity-compose") diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt index 7dd6ee0767..3349f73c68 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/visualtransformations/AmountVisualTransformation.kt @@ -12,7 +12,7 @@ import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_S import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode import com.tangem.core.ui.utils.defaultFormat import com.tangem.core.ui.utils.formatWithThousands -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.text.DecimalFormat import java.text.NumberFormat import java.util.Locale @@ -78,7 +78,7 @@ class AmountVisualTransformation( currency = formatterCurrency } val formatter = requireNotNull(numberFormatter as? DecimalFormat) { - Timber.e("NumberFormat is null") + TangemLogger.e("NumberFormat is null") return AnnotatedString(BigDecimalFormatConstants.EMPTY_BALANCE_SIGN) } return buildAnnotatedString { diff --git a/core/utils/build.gradle.kts b/core/utils/build.gradle.kts index 48e632eff4..0d59823349 100644 --- a/core/utils/build.gradle.kts +++ b/core/utils/build.gradle.kts @@ -23,6 +23,10 @@ dependencies { implementation(deps.jodatime) // endregion + // region Logging + implementation(deps.kermit) + // endregion + testImplementation(deps.test.coroutine) testImplementation(deps.test.junit5) testRuntimeOnly(deps.test.junit5.engine) diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/FeatureCoroutineExceptionHandler.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/FeatureCoroutineExceptionHandler.kt index 9b92b1cf18..582d6d4b57 100644 --- a/core/utils/src/main/java/com/tangem/utils/coroutines/FeatureCoroutineExceptionHandler.kt +++ b/core/utils/src/main/java/com/tangem/utils/coroutines/FeatureCoroutineExceptionHandler.kt @@ -1,10 +1,9 @@ package com.tangem.utils.coroutines +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.CoroutineExceptionHandler import java.io.PrintWriter import java.io.StringWriter -import java.util.logging.Level -import java.util.logging.Logger /** [REDACTED_AUTHOR] @@ -16,11 +15,7 @@ object FeatureCoroutineExceptionHandler { val sw = StringWriter() throwable.printStackTrace(PrintWriter(sw)) val exceptionAsString: String = sw.toString() - // it delegates logging to android Logger, cause cant use timber in java module - Logger.getLogger("CoroutineExceptHandler").log( - Level.INFO, - "CoroutineException: from: $from, exception: $exceptionAsString", - ) + TangemLogger.i("CoroutineException: from: $from, exception: $exceptionAsString") throw throwable } } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/logging/TangemLogger.kt b/core/utils/src/main/java/com/tangem/utils/logging/TangemLogger.kt new file mode 100644 index 0000000000..5bd1fab06f --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/logging/TangemLogger.kt @@ -0,0 +1,63 @@ +package com.tangem.utils.logging + +import co.touchlab.kermit.Logger + +/** + * Application-level logger that wraps Kermit [Logger] with the same API. + * All modules should use [TangemLogger] instead of importing Kermit directly. + */ +object TangemLogger { + + fun v(messageString: String, throwable: Throwable? = null) { + Logger.v(messageString, throwable) + } + + fun d(messageString: String, throwable: Throwable? = null) { + Logger.d(messageString, throwable) + } + + fun i(messageString: String, throwable: Throwable? = null) { + Logger.i(messageString, throwable) + } + + fun w(messageString: String, throwable: Throwable? = null) { + Logger.w(messageString, throwable) + } + + fun e(messageString: String, throwable: Throwable? = null) { + Logger.e(messageString, throwable) + } + + fun a(messageString: String, throwable: Throwable? = null) { + Logger.a(messageString, throwable) + } + + fun withTag(tag: String): TaggedLogger = TaggedLogger(tag) + + class TaggedLogger internal constructor(private val tag: String) { + + fun v(messageString: String, throwable: Throwable? = null) { + Logger.withTag(tag).v(messageString, throwable) + } + + fun d(messageString: String, throwable: Throwable? = null) { + Logger.withTag(tag).d(messageString, throwable) + } + + fun i(messageString: String, throwable: Throwable? = null) { + Logger.withTag(tag).i(messageString, throwable) + } + + fun w(messageString: String, throwable: Throwable? = null) { + Logger.withTag(tag).w(messageString, throwable) + } + + fun e(messageString: String, throwable: Throwable? = null) { + Logger.withTag(tag).e(messageString, throwable) + } + + fun a(messageString: String, throwable: Throwable? = null) { + Logger.withTag(tag).a(messageString, throwable) + } + } +} \ No newline at end of file diff --git a/data/account/build.gradle.kts b/data/account/build.gradle.kts index a07d7fac8a..61664f301e 100644 --- a/data/account/build.gradle.kts +++ b/data/account/build.gradle.kts @@ -63,7 +63,6 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) // endregion // region Test diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcher.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcher.kt index b655bbbdcc..adb70a2b7e 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcher.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcher.kt @@ -7,9 +7,9 @@ import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch -import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -30,7 +30,7 @@ internal class DefaultMultiAccountListFetcher @Inject constructor( when (params) { is MultiAccountListFetcher.Params.Set -> { if (params.ids.isEmpty()) { - Timber.d("No wallet ids provided to fetch accounts.") + TangemLogger.d("No wallet ids provided to fetch accounts.") return@either } @@ -52,7 +52,7 @@ internal class DefaultMultiAccountListFetcher @Inject constructor( "Failed to fetch accounts for wallets:\n${errors.entries.joinToString(separator = "\n")}", ) - Timber.e(exception) + TangemLogger.e("Error", exception) raise(exception) } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt index 0e0b010f72..361622055b 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt @@ -16,7 +16,7 @@ import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResp import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse import com.tangem.domain.models.wallet.UserWalletId -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import javax.inject.Singleton @@ -60,7 +60,7 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( ): FetchResult { val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED) if (isResponseUpToDate) { - Timber.e("ETag is up to date, no need to update accounts for wallet: $userWalletId") + TangemLogger.e("ETag is up to date, no need to update accounts for wallet: $userWalletId") val response = requireNotNull(savedAccountsResponse) { "Saved accounts response is null for wallet: $userWalletId" } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt index ef589cc97d..65e2f11bde 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -32,10 +32,10 @@ import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.replaceBy +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -import timber.log.Timber /** [REDACTED_AUTHOR] @@ -174,7 +174,7 @@ internal class DefaultAccountsCRUDRepository( val response = getAccountsResponseSync(userWalletId = userWalletId) if (response == null) { - Timber.e("Can't sync tokens. No accounts response found for wallet: $userWalletId") + TangemLogger.e("Can't sync tokens. No accounts response found for wallet: $userWalletId") return } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt b/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt index b48fff6967..36343bcd89 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/tokens/DefaultMainAccountTokensMigration.kt @@ -20,12 +20,12 @@ import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse import com.tangem.datasource.local.accounts.AccountTokenMigrationStore import com.tangem.datasource.utils.getSyncOrNull import com.tangem.domain.account.tokens.MainAccountTokensMigration -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.wallet.UserWalletId import com.tangem.lib.crypto.derivation.AccountNodeRecognizer +import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.launch -import timber.log.Timber /** * Implementation of [MainAccountTokensMigration] for migrating tokens associated with a main account. @@ -49,7 +49,7 @@ internal class DefaultMainAccountTokensMigration( val response = store.getSyncOrNull() ensureNotNull(response) { val exception = IllegalStateException("No cached accounts response found") - Timber.e(exception) + TangemLogger.e("Error", exception) exception } val mainAccount = findAccount(response = response, derivationIndex = DerivationIndex.Main) @@ -57,7 +57,7 @@ internal class DefaultMainAccountTokensMigration( .filterNot { accountDTO -> accountDTO.derivationIndex.toDerivationIndex().isMain } if (customAccounts.isEmpty()) { - Timber.i("There is only the Main account. Nothing to migrate") + TangemLogger.i("There is only the Main account. Nothing to migrate") return@either response } @@ -66,7 +66,7 @@ internal class DefaultMainAccountTokensMigration( .filter { it.key.value in customAccountIndexes } if (unassignedTokens.isEmpty()) { - Timber.i("No unassigned tokens found for migration") + TangemLogger.i("No unassigned tokens found for migration") return@either response } @@ -103,7 +103,7 @@ internal class DefaultMainAccountTokensMigration( derivationIndex: DerivationIndex, ): Either = either { if (derivationIndex == DerivationIndex.Main) { - Timber.i("Migration skipped: derivation index is Main") + TangemLogger.i("Migration skipped: derivation index is Main") return@either } @@ -113,7 +113,7 @@ internal class DefaultMainAccountTokensMigration( ensureNotNull(response) { val exception = IllegalStateException("No cached accounts response found") - Timber.e(exception) + TangemLogger.e("Error", exception) exception } @@ -123,7 +123,7 @@ internal class DefaultMainAccountTokensMigration( val unassignedTokens = mainAccount.findUnassignedTokens(derivationIndex) if (unassignedTokens.isNullOrEmpty()) { - Timber.i("No unassigned tokens found for migration") + TangemLogger.i("No unassigned tokens found for migration") return@either } @@ -158,7 +158,7 @@ internal class DefaultMainAccountTokensMigration( return ensureNotNull(account) { val exception = IllegalStateException("No account found with derivation index: $derivationIndex") - Timber.e(exception) + TangemLogger.e("Error", exception) exception } } @@ -198,13 +198,13 @@ internal class DefaultMainAccountTokensMigration( return filter { savedToken -> val blockchain = Blockchain.fromNetworkId(networkId = savedToken.networkId) if (blockchain == null) { - Timber.e("Token has unknown networkId: $savedToken") + TangemLogger.e("Token has unknown networkId: $savedToken") return@filter false } val derivationPathValue = savedToken.derivationPath if (derivationPathValue == null) { - Timber.e("Token has no derivation path: $savedToken") + TangemLogger.e("Token has no derivation path: $savedToken") return@filter false } @@ -212,7 +212,7 @@ internal class DefaultMainAccountTokensMigration( val accountNodeValue = accountNodeRecognizer.recognize(derivationPathValue) if (accountNodeValue == null) { - Timber.e("Token has unrecognized derivation path: $savedToken") + TangemLogger.e("Token has unrecognized derivation path: $savedToken") return@filter false } @@ -230,7 +230,7 @@ internal class DefaultMainAccountTokensMigration( eTagsStore.clear(userWalletId = userWalletId, key = ETagsStore.Key.WalletAccounts) } val exception = IllegalStateException("Failed to push updated tokens after migration") - Timber.e(exception) + TangemLogger.e("Error", exception) raise(exception) }, ) diff --git a/data/analytics/build.gradle.kts b/data/analytics/build.gradle.kts index 279caa054f..1ad08811c1 100644 --- a/data/analytics/build.gradle.kts +++ b/data/analytics/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { /** Project - Analytics */ implementation(projects.core.analytics.models) + implementation(projects.core.utils) /** Project - Data */ implementation(projects.core.datasource) @@ -32,5 +33,4 @@ dependencies { /** Other */ implementation(deps.kotlin.coroutines) - implementation(deps.timber) } \ No newline at end of file diff --git a/data/app-currency/build.gradle.kts b/data/app-currency/build.gradle.kts index ca214f10a9..719c23cae7 100644 --- a/data/app-currency/build.gradle.kts +++ b/data/app-currency/build.gradle.kts @@ -33,5 +33,4 @@ dependencies { /** Other */ implementation(deps.jodatime) implementation(deps.kotlin.coroutines) - implementation(deps.timber) } \ No newline at end of file diff --git a/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/FlipListener.kt b/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/FlipListener.kt index 292a209dc4..7afc9d5717 100644 --- a/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/FlipListener.kt +++ b/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/FlipListener.kt @@ -25,10 +25,10 @@ internal class FlipListener(private val action: () -> Unit) : SensorEventListene isScreenDown = true lastTriggerTime = currentTime // TODO add module logging - // Timber.tag("onSensorChanged").d("screen down") + // Logger.withTag("onSensorChanged").d("screen down") } else if (zAxisValue >= zAxisThreshold) { if (isScreenDown && currentTime - lastTriggerTime <= throttleTimeMs) { - // Timber.tag("onSensorChanged").d("screen up!") + // Logger.withTag("onSensorChanged").d("screen up!") lastTriggerTime = currentTime action.invoke() } diff --git a/data/common/build.gradle.kts b/data/common/build.gradle.kts index cb2baa1194..e8cda44907 100644 --- a/data/common/build.gradle.kts +++ b/data/common/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.jodatime) implementation(deps.kotlin.coroutines) - implementation(deps.timber) /* Test */ testImplementation(projects.common.test) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt b/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt index 4cdf5d1178..b3b0576829 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt @@ -4,8 +4,8 @@ import arrow.core.raise.Raise import arrow.core.raise.recover import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withTimeoutOrNull -import timber.log.Timber import kotlin.time.Duration /** @@ -67,7 +67,7 @@ suspend inline fun safeApiCall( ): T = recover( block = { call(ApiResponseRaise(raise = this)) }, recover = { error -> - Timber.e(error, "Unable to perform safe API call") + TangemLogger.e("Unable to perform safe API call", error) onError(error) }, ) \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt index 3ad4dfe46c..ecd52fa3ac 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/DefaultCacheRegistry.kt @@ -2,13 +2,13 @@ package com.tangem.data.common.cache import com.tangem.datasource.local.cache.CacheKeysStore import com.tangem.datasource.local.cache.model.CacheKey +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.joda.time.Duration import org.joda.time.LocalDateTime -import timber.log.Timber import java.util.concurrent.ConcurrentHashMap internal class DefaultCacheRegistry( @@ -26,17 +26,17 @@ internal class DefaultCacheRegistry( } override suspend fun invalidate(key: String) { - Timber.d("Invalidate the cache key: $key") + TangemLogger.d("Invalidate the cache key: $key") withContext(NonCancellable) { cacheKeysStore.remove(key) } } override suspend fun invalidate(keys: Collection) { - Timber.d("Invalidate cache keys: $keys") + TangemLogger.d("Invalidate cache keys: $keys") withContext(NonCancellable) { cacheKeysStore.remove(keys) } } override suspend fun invalidateAll() { - Timber.d("Invalidate all cache keys") + TangemLogger.d("Invalidate all cache keys") withContext(NonCancellable) { cacheKeysStore.clear() } } @@ -58,7 +58,7 @@ internal class DefaultCacheRegistry( } try { - Timber.d("Invoke the action associated with the cache key: $key") + TangemLogger.d("Invoke the action associated with the cache key: $key") cacheKeysStore.store( key = CacheKey( @@ -70,7 +70,7 @@ internal class DefaultCacheRegistry( block() } catch (e: Throwable) { - Timber.e(e, "The action related to the cache key has failed: $key") + TangemLogger.e("The action related to the cache key has failed: $key", e) invalidate(key) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/cache/etag/DefaultETagsStore.kt b/data/common/src/main/kotlin/com/tangem/data/common/cache/etag/DefaultETagsStore.kt index 3fe201e5de..59a7d62ae3 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/cache/etag/DefaultETagsStore.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/cache/etag/DefaultETagsStore.kt @@ -6,7 +6,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store import com.tangem.domain.models.wallet.UserWalletId -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Default implementation of the [ETagsStore] interface for managing ETag values @@ -25,7 +25,7 @@ internal class DefaultETagsStore( override suspend fun store(userWalletId: UserWalletId, key: ETagsStore.Key, value: String) { if (value.isBlank()) { - Timber.e("ETag value is blank, not storing it. userWalletId: $userWalletId, key: $key") + TangemLogger.e("ETag value is blank, not storing it. userWalletId: $userWalletId, key: $key") return } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt index 14bb34f708..4b7cffe26c 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt @@ -11,7 +11,7 @@ import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import com.tangem.blockchain.common.Token as SdkToken // FIXME: Make internal @@ -52,7 +52,7 @@ class CryptoCurrencyFactory( accountIndex: DerivationIndex? = null, ): CryptoCurrency.Token? { if (blockchain == Blockchain.Unknown) { - Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") + TangemLogger.e("Unable to map the SDK token to the domain token with Unknown blockchain") return null } @@ -93,7 +93,7 @@ class CryptoCurrencyFactory( accountIndex = accountIndex, ) } else { - Timber.e("Unable to get blockchain from chainId == $chainId") + TangemLogger.e("Unable to get blockchain from chainId == $chainId") null } } @@ -105,7 +105,7 @@ class CryptoCurrencyFactory( accountIndex: DerivationIndex? = null, ): CryptoCurrency.Coin? { if (blockchain == Blockchain.Unknown) { - Timber.e("Unable to map the SDK token to the domain token with Unknown blockchain") + TangemLogger.e("Unable to map the SDK token to the domain token with Unknown blockchain") return null } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt index 06bc2f1e85..a939336175 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/ResponseCryptoCurrenciesFactory.kt @@ -11,7 +11,7 @@ import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import com.tangem.blockchain.common.Token as SdkToken @@ -46,7 +46,7 @@ class ResponseCryptoCurrenciesFactory @Inject constructor( ): CryptoCurrency? { var blockchain = Blockchain.fromNetworkId(responseToken.networkId) if (blockchain == null || blockchain == Blockchain.Unknown) { - Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}") + TangemLogger.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}") return null } @@ -71,7 +71,7 @@ class ResponseCryptoCurrenciesFactory @Inject constructor( ): CryptoCurrency? { var blockchain = Blockchain.fromNetworkId(responseToken.networkId) if (blockchain == null || blockchain == Blockchain.Unknown) { - Timber.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}") + TangemLogger.e("Unable to find a blockchain with the network ID: ${responseToken.networkId}") return null } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt index 3594f6f1a0..6a21673603 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseAccountIdEnricher.kt @@ -8,7 +8,7 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.wallet.UserWalletId import com.tangem.lib.crypto.derivation.AccountNodeRecognizer -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Enriches the [UserTokensResponse] with accountId values for tokens @@ -61,20 +61,20 @@ object UserTokensResponseAccountIdEnricher { .groupBy { savedToken -> val derivationPathValue = savedToken.derivationPath if (derivationPathValue == null) { - Timber.e("Token $savedToken has no derivation path") + TangemLogger.e("Token $savedToken has no derivation path") return@groupBy null } val blockchain = Blockchain.fromNetworkId(networkId = savedToken.networkId) if (blockchain == null) { - Timber.e("Token $savedToken has unknown networkId") + TangemLogger.e("Token $savedToken has unknown networkId") return@groupBy null } val accountNodeRecognizer = AccountNodeRecognizer(blockchain) val accountIndex = accountNodeRecognizer.recognize(derivationPathValue) if (accountIndex == null) { - Timber.e("Token $savedToken has unrecognized derivation path") + TangemLogger.e("Token $savedToken has unrecognized derivation path") return@groupBy null } @@ -89,7 +89,7 @@ object UserTokensResponseAccountIdEnricher { if (accountIndex == null) return@mapKeys null val derivationIndex = DerivationIndex.invoke(value = accountIndex.toInt()).getOrElse { - Timber.e("Failed to parse derivation index from account index: $accountIndex") + TangemLogger.e("Failed to parse derivation index from account index: $accountIndex") return@mapKeys null } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt index 5796a28601..a745dfe274 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt @@ -12,10 +12,10 @@ import com.tangem.domain.common.wallets.getSyncOrNull import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import com.tangem.utils.retryer.Retryer import com.tangem.utils.retryer.RetryerPool import kotlinx.coroutines.withContext -import timber.log.Timber @Suppress("LongParameterList") class UserTokensSaver( @@ -36,7 +36,7 @@ class UserTokensSaver( val userWallet = userWalletsListRepository.getSyncOrNull(id = userWalletId) if (userWallet == null) { - Timber.e("UserWallet with id $userWalletId not found. Cannot push tokens.") + TangemLogger.e("UserWallet with id $userWalletId not found. Cannot push tokens.") onFailSend() return@withContext } @@ -116,7 +116,7 @@ class UserTokensSaver( userWalletId = userWalletId, response = response, onFailSend = { - Timber.e( + TangemLogger.e( "Retryer: Failed to push updated tokens on attempt ${iteration + 1} for $userWalletId", ) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt index bf9014a844..97ba053720 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt @@ -13,7 +13,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.lib.crypto.derivation.toMutable -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -154,11 +154,11 @@ class NetworkFactory @Inject constructor( private fun Blockchain.isBlockchainSupported(): Boolean { if (this == Blockchain.Unknown) { - Timber.w("Unable to convert Unknown blockchain to the domain network model") + TangemLogger.w("Unable to convert Unknown blockchain to the domain network model") return false } if (this in excludedBlockchains) { - Timber.w("Unable to convert excluded blockchain to the domain network model") + TangemLogger.w("Unable to convert excluded blockchain to the domain network model") return false } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/quote/DefaultQuotesFetcher.kt b/data/common/src/main/kotlin/com/tangem/data/common/quote/DefaultQuotesFetcher.kt index 91f3f11426..a3c47ae0f1 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/quote/DefaultQuotesFetcher.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/quote/DefaultQuotesFetcher.kt @@ -15,11 +15,11 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.domain.core.utils.eitherOn import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import org.joda.time.DateTime -import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration.Companion.seconds @@ -194,7 +194,7 @@ internal class DefaultQuotesFetcher( return this } - Timber.d("Some quotes are missing from the server response: $skippedIds") + TangemLogger.d("Some quotes are missing from the server response: $skippedIds") val emptyQuotes = skippedIds.associateWith { QuotesResponse.Quote.EMPTY } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt b/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt index 710a982835..a1b8af7782 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/utils/RequestUtils.kt @@ -1,10 +1,10 @@ package com.tangem.data.common.utils +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive import kotlinx.coroutines.yield -import timber.log.Timber import kotlin.coroutines.cancellation.CancellationException @Suppress("UnconditionalJumpStatementInLoop", "MagicNumber") @@ -19,7 +19,7 @@ suspend fun retryOnError(priority: Boolean = false, startRetryDelay: Int = 5 currentCoroutineContext().ensureActive() } - Timber.e(e, "Error occurred during retryOnError block") + TangemLogger.e("Error occurred during retryOnError block", e) if (priority && priorityCounter > 0) { --priorityCounter diff --git a/data/earn/build.gradle.kts b/data/earn/build.gradle.kts index 985868438d..87173ffe7a 100644 --- a/data/earn/build.gradle.kts +++ b/data/earn/build.gradle.kts @@ -37,7 +37,6 @@ dependencies { // region Other libraries implementation(deps.androidx.datastore) implementation(deps.moshi.kotlin) - implementation(deps.timber) implementation(tangemDeps.blockchain) // endregion diff --git a/data/express/build.gradle.kts b/data/express/build.gradle.kts index f75d1dde55..473892eedf 100644 --- a/data/express/build.gradle.kts +++ b/data/express/build.gradle.kts @@ -30,7 +30,6 @@ dependencies { /** Other */ implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt b/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt index 31d7577acf..360ebda6c1 100644 --- a/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt +++ b/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt @@ -12,7 +12,7 @@ import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.models.wallet.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.filterIf -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultExpressRepository( private val tangemExpressApi: TangemExpressApi, @@ -36,7 +36,7 @@ internal class DefaultExpressRepository( .filterIf(filterProviderTypes.isNotEmpty()) { it.type in filterProviderTypes } }, onError = { error -> - Timber.w(error, "Unable to fetch express providers") + TangemLogger.w("Unable to fetch express providers", error) throw error }, ) diff --git a/data/express/src/main/java/com/tangem/data/express/DefaultExpressServiceFetcher.kt b/data/express/src/main/java/com/tangem/data/express/DefaultExpressServiceFetcher.kt index 6e97e2c7b5..449959c978 100644 --- a/data/express/src/main/java/com/tangem/data/express/DefaultExpressServiceFetcher.kt +++ b/data/express/src/main/java/com/tangem/data/express/DefaultExpressServiceFetcher.kt @@ -22,11 +22,11 @@ import com.tangem.domain.express.models.ExpressAsset import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.update -import timber.log.Timber import javax.inject.Inject typealias InitializationStatusFlow = MutableStateFlow>> @@ -85,7 +85,7 @@ internal class DefaultExpressServiceFetcher @Inject constructor( if (expressAssetsStore.getSyncOrNull(userWallet.walletId) == null) { initializationStatus.update { e.lceError() } } - Timber.e(e, "Unable to fetch assets for: ${userWallet.walletId.stringValue}") + TangemLogger.e("Unable to fetch assets for: ${userWallet.walletId.stringValue}", e) throw e } } diff --git a/data/feedback/build.gradle.kts b/data/feedback/build.gradle.kts index 08b86b9db4..c5ea5e524e 100644 --- a/data/feedback/build.gradle.kts +++ b/data/feedback/build.gradle.kts @@ -25,7 +25,6 @@ dependencies { // endregion // Other libraries - implementation(deps.timber) // endregion // region Core modules diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index ca09522466..5f39f9f870 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -14,10 +14,10 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.utils.logging.TangemLogger import com.tangem.utils.version.AppVersionProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -import timber.log.Timber import java.io.File /** @@ -102,7 +102,7 @@ internal class DefaultFeedbackRepository( override fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? { return blockchainsErrors.value[userWalletId].also { - if (it == null) Timber.e("Blockchain error info is null for $userWalletId") + if (it == null) TangemLogger.e("Blockchain error info is null for $userWalletId") } } diff --git a/data/manage-tokens/build.gradle.kts b/data/manage-tokens/build.gradle.kts index ce17fcef05..fa149015fa 100644 --- a/data/manage-tokens/build.gradle.kts +++ b/data/manage-tokens/build.gradle.kts @@ -51,7 +51,6 @@ dependencies { /** Other */ implementation(deps.moshi.kotlin) - implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) } \ No newline at end of file diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index efdfc59129..c6316183e1 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -22,7 +22,7 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.lib.crypto.BlockchainUtils -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class ManagedCryptoCurrencyFactory( private val networkFactory: NetworkFactory, @@ -223,7 +223,7 @@ internal class ManagedCryptoCurrencyFactory( blockchain.canHandleTokens() -> { val formattedContractAddress = blockchain.reformatContractAddress(contractAddress) if (formattedContractAddress == null) { - Timber.w("Couldn't reformat $contractAddress") + TangemLogger.w("Couldn't reformat $contractAddress") return null } SourceNetwork.Default( diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts index 1678a61ead..bce4fe8a70 100644 --- a/data/markets/build.gradle.kts +++ b/data/markets/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { implementation(deps.jodatime) implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) implementation(tangemDeps.blockchain) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) diff --git a/data/networks/build.gradle.kts b/data/networks/build.gradle.kts index 90ca5576e0..d90e4e2a52 100644 --- a/data/networks/build.gradle.kts +++ b/data/networks/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { // region Other libraries implementation(deps.androidx.datastore) implementation(deps.moshi) - implementation(deps.timber) // endregion // region Tests diff --git a/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAddressConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAddressConverter.kt index 8c3fbae1a2..6585752203 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAddressConverter.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAddressConverter.kt @@ -3,7 +3,7 @@ package com.tangem.data.networks.converters import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.domain.models.network.NetworkAddress import com.tangem.utils.converter.TwoWayConverter -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Converter from [NetworkAddressConverter.Value] to [NetworkAddress] and vice versa @@ -58,7 +58,7 @@ internal object NetworkAddressConverter : TwoWayConverter + TangemLogger.e("Failed to fetch network status for $userWalletId [${network.rawId}]", throwable) networksStatusesStore.setSourceAsOnlyCache(userWalletId = userWalletId, network = network) } } diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt index 8406342ea9..94e3c3b9cc 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt @@ -10,11 +10,11 @@ import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.* -import timber.log.Timber /** * Default implementation of [MultiNetworkStatusProducer] @@ -43,7 +43,7 @@ internal class DefaultMultiNetworkStatusProducer @AssistedInject constructor( val userWallet = userWalletsListRepository.getSyncOrNull(params.userWalletId) if (userWallet == null) { - Timber.e("Unable to get UserWallet with provided ID: ${params.userWalletId}") + TangemLogger.e("Unable to get UserWallet with provided ID: ${params.userWalletId}") return@mapNotNull null } diff --git a/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt b/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt index 4755e55248..b0dc9c6af7 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt @@ -1,9 +1,9 @@ package com.tangem.data.networks.repository -import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory import com.tangem.data.networks.store.NetworksStatusesStore import com.tangem.data.networks.store.storeStatus import com.tangem.data.networks.utils.NetworkStatusFactory +import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.CryptoCurrencyAddress import com.tangem.domain.models.network.Network @@ -11,8 +11,8 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber /** * Default implementation of [NetworksRepository] @@ -36,8 +36,8 @@ internal class DefaultNetworksRepository( val currencies = runCatching { cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) } - .getOrElse { - Timber.e(it, "Unable to create wallet currencies") + .getOrElse { error -> + TangemLogger.e("Unable to create wallet currencies", error) return@withContext } @@ -50,8 +50,8 @@ internal class DefaultNetworksRepository( network: Network, ): List { return runCatching { cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) } - .getOrElse { - Timber.e(it, "Unable to create wallet currencies") + .getOrElse { error -> + TangemLogger.e("Unable to create wallet currencies", error) return emptyList() } .map { currency -> @@ -67,8 +67,8 @@ internal class DefaultNetworksRepository( network: Network.RawID, ): List { return runCatching { cardCryptoCurrencyFactory.createByRawId(userWalletId = userWalletId, network = network) } - .getOrElse { - Timber.e(it, "Unable to create wallet currencies") + .getOrElse { error -> + TangemLogger.e("Unable to create wallet currencies", error) return emptyList() } .map { currency -> diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt index 0cb7dba076..05b161810f 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt @@ -7,15 +7,17 @@ import com.tangem.data.networks.converters.SimpleNetworkStatusConverter import com.tangem.data.networks.models.SimpleNetworkStatus import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.network.entity.NetworkStatusDM -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.models.StatusSource import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.extensions.addOrReplace -import kotlinx.coroutines.* +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* -import timber.log.Timber +import kotlinx.coroutines.launch import java.io.File internal typealias WalletIdWithSimpleStatus = Map> @@ -45,7 +47,7 @@ internal class DefaultNetworksStatusesStore( oldFile.delete() } } catch (e: Exception) { - Timber.e(e, "Error while deleting old networks statuses datastore file") + TangemLogger.e("Error while deleting old networks statuses datastore file", e) } val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch @@ -89,7 +91,7 @@ internal class DefaultNetworksStatusesStore( ifNotFound: (Network.ID) -> SimpleNetworkStatus?, ) { if (networks.isEmpty()) { - Timber.d("Nothing to update: networks are empty") + TangemLogger.d("Nothing to update: networks are empty") return } diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/NetworkStatusesStoreExt.kt b/data/networks/src/main/java/com/tangem/data/networks/store/NetworkStatusesStoreExt.kt index b1da135fa9..6edd224074 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/NetworkStatusesStoreExt.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/NetworkStatusesStoreExt.kt @@ -5,7 +5,7 @@ import com.tangem.domain.models.StatusSource import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.wallet.UserWalletId -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Store actual network [status] by [userWalletId]. @@ -41,14 +41,14 @@ internal suspend fun NetworksStatusesStore.storeStatus(userWalletId: UserWalletI internal suspend fun NetworksStatusesStore.storeSuccess(userWalletId: UserWalletId, status: NetworkStatus) { if (status.value is NetworkStatus.Unreachable) { val message = "Use storeError method to save unreachable status" - Timber.d(message) + TangemLogger.d(message) error(message) } if (status.value.source != StatusSource.ACTUAL) { val message = "Method storeActual can be called only with StatusSource.ACTUAL" - Timber.d(message) + TangemLogger.d(message) error(message) } diff --git a/data/networks/src/main/java/com/tangem/data/networks/utils/DefaultNetworksCleaner.kt b/data/networks/src/main/java/com/tangem/data/networks/utils/DefaultNetworksCleaner.kt index b395990e65..59058d09c2 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/utils/DefaultNetworksCleaner.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/utils/DefaultNetworksCleaner.kt @@ -8,10 +8,10 @@ import com.tangem.domain.networks.utils.NetworksCleaner import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext -import timber.log.Timber /** * Default implementation of [NetworksCleaner]. @@ -30,7 +30,7 @@ internal class DefaultNetworksCleaner( override suspend fun invoke(userWalletId: UserWalletId, currencies: List) { if (currencies.isEmpty()) { - Timber.d("No currencies to clear for wallet: $userWalletId") + TangemLogger.d("No currencies to clear for wallet: $userWalletId") return } @@ -49,7 +49,7 @@ internal class DefaultNetworksCleaner( runSuspendCatching { networksStatusesStore.clear(userWalletId = userWalletId, networks = networks) } - .onFailure { Timber.e(it, "Failed to clear network statuses for wallet: $userWalletId") } + .onFailure { TangemLogger.e("Failed to clear network statuses for wallet: $userWalletId", it) } } } @@ -63,7 +63,7 @@ internal class DefaultNetworksCleaner( walletManagersFacade.remove(userWalletId = userWalletId, networks = networks) } .onFailure { - Timber.e(it, "Failed to remove networks from Blockchain SDK for wallet: $userWalletId") + TangemLogger.e("Failed to remove networks from Blockchain SDK for wallet: $userWalletId", it) } } @@ -72,7 +72,7 @@ internal class DefaultNetworksCleaner( walletManagersFacade.removeTokens(userWalletId = userWalletId, tokens = tokens) } .onFailure { - Timber.e(it, "Failed to remove tokens from Blockchain SDK for wallet: $userWalletId") + TangemLogger.e("Failed to remove tokens from Blockchain SDK for wallet: $userWalletId", it) } } } diff --git a/data/networks/src/main/java/com/tangem/data/networks/utils/NetworkStatusFactory.kt b/data/networks/src/main/java/com/tangem/data/networks/utils/NetworkStatusFactory.kt index 9c9742678c..e94de7efb5 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/utils/NetworkStatusFactory.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/utils/NetworkStatusFactory.kt @@ -9,7 +9,7 @@ import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.yield.supply.YieldSupplyStatus -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** Factory for creating [NetworkStatus] */ object NetworkStatusFactory { @@ -104,7 +104,7 @@ object NetworkStatusFactory { } if (amount == null) { - Timber.w("Unable to find amount for cryptocurrency: $currency") + TangemLogger.w("Unable to find amount for cryptocurrency: $currency") currency.id to NetworkStatus.Amount.NotFound } else { currency.id to NetworkStatus.Amount.Loaded(amount.value) @@ -129,7 +129,7 @@ object NetworkStatusFactory { } if (amount == null) { - Timber.w("Unable to find amount for cryptocurrency: $currency") + TangemLogger.w("Unable to find amount for cryptocurrency: $currency") currency.id to null } else { currency.id to amount.yieldSupplyStatus @@ -191,7 +191,7 @@ object NetworkStatusFactory { } if (address.value.isBlank()) { - Timber.w("Address value is blank") + TangemLogger.w("Address value is blank") } return NetworkAddress.Address(address.value, type) diff --git a/data/news/build.gradle.kts b/data/news/build.gradle.kts index 86796fcedd..0d06bf693d 100644 --- a/data/news/build.gradle.kts +++ b/data/news/build.gradle.kts @@ -39,7 +39,6 @@ dependencies { // region Other libraries implementation(deps.androidx.datastore) implementation(deps.moshi.kotlin) - implementation(deps.timber) // endregion // region Tests diff --git a/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt b/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt index 3e3ef70c45..293cc63837 100644 --- a/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt +++ b/data/news/src/main/java/com/tangem/data/news/repository/DefaultNewsRepository.kt @@ -28,9 +28,9 @@ import com.tangem.pagination.exception.EndOfPaginationException import com.tangem.pagination.fetcher.BatchFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import timber.log.Timber /** * Implementation of [NewsRepository]. @@ -72,7 +72,7 @@ internal class DefaultNewsRepository( response.items }, onError = { error -> - Timber.e(error, "Failed to get list of news") + TangemLogger.e("Failed to get list of news", error) throw error }, ) @@ -224,8 +224,7 @@ internal class DefaultNewsRepository( return withContext(dispatchers.io) { when (val apiResponse = newsApi.getTrendingNews(limit = limit, language = language)) { is ApiResponse.Error -> { - Timber.e( - apiResponse.cause, + TangemLogger.e( "Trending news fetch failed cause: ${ when (val error = apiResponse.cause) { is ApiResponseError.HttpException -> error.code @@ -234,6 +233,7 @@ internal class DefaultNewsRepository( is ApiResponseError.UnknownException -> "UnknownException" } }", + apiResponse.cause, ) trendingNewsStore.clear() trendingNewsStore.store( diff --git a/data/nft/build.gradle.kts b/data/nft/build.gradle.kts index f53acc71ce..63a318a877 100644 --- a/data/nft/build.gradle.kts +++ b/data/nft/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.arrow.fx) implementation(deps.jodatime) - implementation(deps.timber) implementation(deps.androidx.paging.runtime) implementation(deps.moshi.kotlin) ksp(deps.moshi.kotlin.codegen) diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index 7b022349ef..3baa26a3f4 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -34,6 +34,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.logging.TangemLogger import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* @@ -42,7 +43,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset @@ -241,7 +241,7 @@ internal class DefaultNFTRepository @Inject constructor( // NFTCleaner implementation override suspend fun invoke(userWalletId: UserWalletId, networks: Set) { if (networks.isEmpty()) { - Timber.d("No networks to clear for wallet: $userWalletId") + TangemLogger.d("No networks to clear for wallet: $userWalletId") return } @@ -252,7 +252,7 @@ internal class DefaultNFTRepository @Inject constructor( // nftRuntimeStoreFactory.provide(network = network).clear() } .onFailure { throwable -> - Timber.e(throwable, "Failed to clear NFT data for network $network for wallet: $userWalletId") + TangemLogger.e("Failed to clear NFT data for network $network for wallet: $userWalletId", throwable) } } } diff --git a/data/onramp/build.gradle.kts b/data/onramp/build.gradle.kts index bf0adddbac..c61541ce6a 100644 --- a/data/onramp/build.gradle.kts +++ b/data/onramp/build.gradle.kts @@ -47,7 +47,6 @@ dependencies { implementation(deps.kotlin.immutable.collections) implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) implementation(deps.kotlin.serialization) diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt index 5f768924a0..e7f7ad38db 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -22,19 +22,15 @@ import com.tangem.domain.card.common.extensions.canHandleToken import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.common.wallets.loadAndGet -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.HotCryptoCurrency import com.tangem.domain.onramp.repositories.HotCryptoRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.runCatching -import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.coroutines.* +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.plus -import timber.log.Timber /** * Default implementation of [HotCryptoRepository] @@ -123,9 +119,9 @@ internal class DefaultHotCryptoRepository( return runCatching(dispatchers.io) { tangemTechApi.getHotCrypto(currencyId = appCurrencyId).getOrThrow() } - .onSuccess { Timber.d("HotCrypto is successfully updated") } + .onSuccess { TangemLogger.d("HotCrypto is successfully updated") } .onFailure { throwable -> - Timber.e(throwable, "Unable to fetch hot crypto") + TangemLogger.e("Unable to fetch hot crypto", throwable) val httpException = throwable as? ApiResponseError.HttpException analyticsEventHandler.send( diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt index bf07c9a118..7fe880aa2b 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt @@ -50,6 +50,7 @@ import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.tokens.model.Amount import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -57,7 +58,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import org.joda.time.DateTime -import timber.log.Timber import java.util.UUID @Suppress("LongParameterList", "LargeClass", "TooManyFunctions") @@ -221,7 +221,7 @@ internal class DefaultOnrampRepository( ).bind() }, onError = { error -> - Timber.w(error, "Unable to fetch onramp payment methods") + TangemLogger.w("Unable to fetch onramp payment methods", error) throw error }, ) @@ -256,7 +256,7 @@ internal class DefaultOnrampRepository( ).bind() }, onError = { error -> - Timber.w(error, "Unable to fetch onramp pairs") + TangemLogger.w("Unable to fetch onramp pairs", error) throw error }, ) @@ -273,7 +273,7 @@ internal class DefaultOnrampRepository( ).bind() }, onError = { error -> - Timber.w(error, "Unable to fetch express providers") + TangemLogger.w("Unable to fetch express providers", error) throw error }, ) @@ -321,7 +321,7 @@ internal class DefaultOnrampRepository( ).bind() }, onError = { error -> - Timber.w(error, "Unable to fetch onramp pairs") + TangemLogger.w("Unable to fetch onramp pairs", error) throw error }, ) @@ -455,7 +455,7 @@ internal class DefaultOnrampRepository( ).bind() }, onError = { e -> - Timber.e(e) + TangemLogger.e("Error", e) throw e }, ) @@ -478,7 +478,7 @@ internal class DefaultOnrampRepository( throw OnrampRedirectError.VerificationFailed } } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) throw e } } @@ -602,7 +602,7 @@ internal class DefaultOnrampRepository( countryCode = countryCode, ) } else { - Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error") + TangemLogger.w("Unable to fetch onramp quotes for ${provider.id}. $error", error) OnrampQuote.Error( paymentMethod = paymentMethod, provider = provider, @@ -612,7 +612,7 @@ internal class DefaultOnrampRepository( ) } } else { - Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error") + TangemLogger.w("Unable to fetch onramp quotes for ${provider.id}. $error", error) null } diff --git a/data/promo/build.gradle.kts b/data/promo/build.gradle.kts index b4ab572c21..e4e45ecdca 100644 --- a/data/promo/build.gradle.kts +++ b/data/promo/build.gradle.kts @@ -14,7 +14,6 @@ dependencies { implementation(deps.androidx.datastore) implementation(deps.jodatime) - implementation(deps.timber) implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/data/quotes/build.gradle.kts b/data/quotes/build.gradle.kts index b4f0191e43..5d169e0702 100644 --- a/data/quotes/build.gradle.kts +++ b/data/quotes/build.gradle.kts @@ -44,7 +44,6 @@ dependencies { // region Other libraries implementation(deps.androidx.datastore) implementation(deps.moshi.kotlin) - implementation(deps.timber) // endregion // region Tests diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt index 1660472cb9..6b1099fb1a 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt @@ -13,7 +13,7 @@ import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import javax.inject.Singleton @@ -37,7 +37,7 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor( override suspend fun invoke(params: MultiQuoteStatusFetcher.Params) = Either.catchOn(dispatchers.default) { if (params.currenciesIds.isEmpty()) { - Timber.d("No currencies to fetch quotes for") + TangemLogger.d("No currencies to fetch quotes for") return@catchOn } @@ -67,7 +67,7 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor( quotesStatusesStore.store(values = updatedResponse.quotes) } .onLeft { throwable -> - Timber.e(throwable) + TangemLogger.e("Error", throwable) quotesStatusesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } @@ -77,7 +77,7 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor( if (appCurrencyId.isNullOrBlank()) { val exception = IllegalStateException("Unable to get AppCurrency for updating quotes") - Timber.e(exception) + TangemLogger.e("Error", exception) throw exception } diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt index f499f8a4a0..cad0c16d50 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteUpdater.kt @@ -4,17 +4,17 @@ import androidx.annotation.VisibleForTesting import arrow.core.left import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.datasource.appcurrency.AppCurrencyResponseStore -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteUpdater +import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* -import timber.log.Timber /** * Default implementation of [MultiQuoteUpdater] which updates quotes when the app currency changes @@ -36,14 +36,14 @@ internal class DefaultMultiQuoteUpdater( private val updaterHolder = JobHolder() override fun subscribe() { - Timber.d("Subscribe on quotes updates") + TangemLogger.d("Subscribe on quotes updates") getMultiQuoteUpdates() .launchIn(coroutineScope) .saveIn(updaterHolder) } override fun unsubscribe() { - Timber.e("Unsubscribe from quotes updates") + TangemLogger.i("Unsubscribe from quotes updates") updaterHolder.cancel() } @@ -63,10 +63,10 @@ internal class DefaultMultiQuoteUpdater( appCurrencyId = appCurrency.id, ), ) - .onLeft(Timber::e) + .onLeft { TangemLogger.e("Error", it) } } .retryWhen { cause, _ -> - Timber.e("Retry updating quotes: $cause") + TangemLogger.e("Retry updating quotes: $cause") emit(cause.left()) diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt b/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt index d170b82c5c..d16b775518 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt @@ -4,7 +4,7 @@ import com.tangem.data.quotes.store.QuotesStatusesStore import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.quotes.QuotesRepository -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Default implementation of [QuotesRepository] @@ -19,7 +19,7 @@ internal class DefaultQuotesRepository( override suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set { if (currenciesIds.isEmpty()) { - Timber.e("currenciesIds are empty") + TangemLogger.e("currenciesIds are empty") return emptySet() } diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt index 3225adfea6..9c4bd521e7 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/store/DefaultQuotesStatusesStore.kt @@ -4,16 +4,16 @@ import androidx.datastore.core.DataStore import com.tangem.data.quotes.converter.QuoteStatusConverter import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.extensions.addOrReplace +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch -import timber.log.Timber internal typealias CurrencyIdWithQuote = Map @@ -64,7 +64,7 @@ internal class DefaultQuotesStatusesStore( ifNotFound: (CryptoCurrency.RawID) -> QuoteStatus?, ) { if (currenciesIds.isEmpty()) { - Timber.d("Nothing to update: currencies ids are empty") + TangemLogger.d("Nothing to update: currencies ids are empty") return } diff --git a/data/settings/build.gradle.kts b/data/settings/build.gradle.kts index 30c7231276..3b71894edc 100644 --- a/data/settings/build.gradle.kts +++ b/data/settings/build.gradle.kts @@ -33,7 +33,6 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) // endregion diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 588d3b71e3..0f77d6749a 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -14,11 +14,11 @@ import com.tangem.domain.settings.usercountry.models.GB_COUNTRY import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext -import timber.log.Timber import java.util.Locale @Suppress("TooManyFunctions") @@ -145,7 +145,7 @@ internal class DefaultSettingsRepository( override fun getUserCountryCode(): StateFlow = userCountryFlow override suspend fun fetchUserCountryCode() { - Timber.i("Start fetching user country code") + TangemLogger.i("Start fetching user country code") // for GB locale avoid request geo and use device default (FCA fixes) if (Locale.getDefault().country == GB_COUNTRY.code) { @@ -166,7 +166,7 @@ internal class DefaultSettingsRepository( else -> UserCountry.Other(code = country) } - Timber.i("User code country is $code") + TangemLogger.i("User code country is $code") userCountryFlow.value = code } diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index fc7059b626..3aa93ac8e2 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -55,7 +55,6 @@ dependencies { implementation(deps.kotlin.immutable.collections) implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) implementation(deps.firebase.crashlytics) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt index 8752d83b0e..b189675a38 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt @@ -5,11 +5,7 @@ import arrow.core.getOrElse import arrow.core.raise.Raise import arrow.core.raise.either import arrow.core.raise.ensure -import com.tangem.data.staking.converters.ethpool.P2PEthPoolBroadcastResultConverter -import com.tangem.data.staking.converters.ethpool.P2PEthPoolErrorConverter -import com.tangem.data.staking.converters.ethpool.P2PEthPoolStakingAccountConverter -import com.tangem.data.staking.converters.ethpool.P2PEthPoolUnsignedTxConverter -import com.tangem.data.staking.converters.ethpool.P2PEthPoolVaultConverter +import com.tangem.data.staking.converters.ethpool.* import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.ethpool.P2PEthPoolApi import com.tangem.datasource.api.ethpool.models.request.P2PEthPoolBroadcastRequest @@ -28,11 +24,11 @@ import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.P2PEthPoolRepository import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -import timber.log.Timber /** * P2PEthPool staking repository implementation @@ -71,7 +67,7 @@ internal class DefaultP2PEthPoolRepository( override suspend fun fetchVaults(network: P2PEthPoolNetwork) { val vaults = if (stakingFeatureToggles.isEthStakingEnabled) { getVaults(network).getOrElse { error -> - Timber.e("Error fetching P2PEthPool vaults: $error") + TangemLogger.e("Error fetching P2PEthPool vaults: $error") emptyList() } } else { diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt index 171c5c720a..79dc265b4f 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt @@ -47,13 +47,13 @@ import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.repositories.StakeKitRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext -import timber.log.Timber @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") internal class DefaultStakeKitRepository( @@ -91,7 +91,7 @@ internal class DefaultStakeKitRepository( when (response) { is ApiResponse.Success -> response.data.data.filter { yield -> yield.isAvailable == true } is ApiResponse.Error -> { - Timber.e("Error fetching enabled yields: ${response.cause}") + TangemLogger.e("Error fetching enabled yields: ${response.cause}") emptyList() } } @@ -142,7 +142,7 @@ internal class DefaultStakeKitRepository( network = networkTypeString, status = actionStatusString, ).getOrThrow().data, - onError = { Timber.e("Error converting staking actions list: $it") }, + onError = { TangemLogger.e("Error converting staking actions list: $it") }, ) } } @@ -348,7 +348,7 @@ internal class DefaultStakeKitRepository( private suspend fun getEnabledYieldsSync(): List { return YieldConverter.convertListIgnoreErrors( input = stakingYieldsStore.getSync(), - onError = { Timber.e("Error converting one of the items in enabled yields: $it") }, + onError = { TangemLogger.e("Error converting one of the items in enabled yields: $it") }, ) } @@ -356,7 +356,7 @@ internal class DefaultStakeKitRepository( return stakingYieldsStore.get().map { yields -> YieldConverter.convertListIgnoreErrors( input = yields, - onError = { Timber.e("Error converting one of the items in enabled yields: $it") }, + onError = { TangemLogger.e("Error converting one of the items in enabled yields: $it") }, ) } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitTransactionHashRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitTransactionHashRepository.kt index 0147b2da07..b0c1a1ad67 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitTransactionHashRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitTransactionHashRepository.kt @@ -1,15 +1,15 @@ package com.tangem.data.staking import com.tangem.datasource.api.stakekit.StakeKitApi -import com.tangem.datasource.api.stakekit.models.request.* +import com.tangem.datasource.api.stakekit.models.request.SubmitTransactionHashRequestBody import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectListSync import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata import com.tangem.domain.staking.repositories.StakeKitTransactionHashRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber internal class DefaultStakeKitTransactionHashRepository( private val stakeKitApi: StakeKitApi, @@ -75,7 +75,7 @@ internal class DefaultStakeKitTransactionHashRepository( append("StakeKit id = ${transaction.transactionId} and\n") append("transaction hash = ${transaction.transactionHash}") } - Timber.e(logMessage) + TangemLogger.e(logMessage) } } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt index c957ac3d12..d329e2bb77 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt @@ -30,11 +30,11 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject /** @@ -66,10 +66,10 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( ) : MultiStakingBalanceFetcher { override suspend fun invoke(params: MultiStakingBalanceFetcher.Params): Either { - Timber.i("Start fetching staking balances for params:\n$params") + TangemLogger.i("Start fetching staking balances for params:\n$params") val stakingIds = params.stakingIds.ifEmpty { - Timber.i("Nothing to fetch, empty stakingIds for ${params.userWalletId}") + TangemLogger.i("Nothing to fetch, empty stakingIds for ${params.userWalletId}") return Unit.right() } @@ -84,7 +84,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( stakingIntegrationID is StakingIntegrationID.StakeKit } - Timber.i( + TangemLogger.i( """ Staking IDs to fetch: - StakeKit: ${stakeKitIds.joinToString()} @@ -104,7 +104,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( } } .onLeft { throwable -> - Timber.e(throwable, "Unable to fetch staking balances $params") + TangemLogger.e("Unable to fetch staking balances $params", throwable) if (stakeKitIds.isNotEmpty()) { stakeKitBalancesStore.storeError( @@ -137,7 +137,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( val vaults = runSuspendCatching { p2pEthPoolVaultsStore.getSync() }.getOrNull().orEmpty() if (vaults.isEmpty()) { - Timber.w("No P2PEthPool vaults available for $userWalletId, storing empty balances") + TangemLogger.w("No P2PEthPool vaults available for $userWalletId, storing empty balances") p2PEthPoolBalancesStore.storeEmpty(userWalletId = userWalletId, stakingIds = stakingIds) return } @@ -155,7 +155,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( val addresses = stakingIds.map { it.address }.toSet() val responses = fetchP2PAccountResponses(vaults = vaults, addresses = addresses) - Timber.i("Successfully fetched ${responses.size} P2PEthPool balances for $userWalletId") + TangemLogger.i("Successfully fetched ${responses.size} P2PEthPool balances for $userWalletId") if (responses.isNotEmpty()) { p2PEthPoolBalancesStore.storeActual(userWalletId = userWalletId, values = responses) @@ -167,19 +167,22 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( } if (missingStakingIds.isNotEmpty()) { - Timber.i("Missing responses for ${missingStakingIds.size} staking IDs: $missingStakingIds") + TangemLogger.i( + "Missing responses for ${missingStakingIds.size} staking IDs:" + + " $missingStakingIds", + ) p2PEthPoolBalancesStore.storeError( userWalletId = userWalletId, stakingIds = missingStakingIds.toSet(), ) } } else { - Timber.i("No P2PEthPool responses received for $userWalletId") + TangemLogger.i("No P2PEthPool responses received for $userWalletId") p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) } }, onError = { throwable -> - Timber.e(throwable, "Unable to fetch P2PEthPool balances $userWalletId") + TangemLogger.e("Unable to fetch P2PEthPool balances $userWalletId", throwable) p2PEthPoolBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) @@ -207,7 +210,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( is ApiResponse.Success -> { val data = response.data if (data.error != null) { - Timber.w( + TangemLogger.w( "P2PEthPool API returned error for vault ${vault.vaultAddress}, " + "address $address: ${data.error ?: "error"}", ) @@ -219,17 +222,17 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( } } is ApiResponse.Error -> { - Timber.w( - response.cause, + TangemLogger.w( "Failed to fetch P2PEthPool balance for vault ${vault.vaultAddress}, " + "address $address", + response.cause, ) } } }.onFailure { error -> - Timber.w( - error, + TangemLogger.w( "Failed to fetch P2PEthPool balance for vault ${vault.vaultAddress}, address $address", + error, ) } } @@ -245,7 +248,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( if (!isSupportedByWallet) { val exception = IllegalStateException("Wallet $userWalletId is not supported: $maybeUserWallet") - Timber.e(exception) + TangemLogger.e("Error", exception) ifNotSupported(exception) } @@ -263,7 +266,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( val availableStakingIds = groupedStakingIds[true].orEmpty() val unavailableStakingIds = groupedStakingIds[false].orEmpty() - Timber.i( + TangemLogger.i( """ Available staking IDs: ${availableStakingIds.joinToString()} Unavailable staking IDs: ${unavailableStakingIds.joinToString()} @@ -282,7 +285,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( – stakingIds: ${stakingIds.joinToString()} """.trimIndent(), ) - Timber.i(exception) + TangemLogger.i(exception.toString()) throw exception } } @@ -293,7 +296,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( if (yieldsIds.isEmpty()) { val exception = IllegalStateException("No enabled yields for $userWalletId") - Timber.e(exception) + TangemLogger.e("Error", exception) throw exception } @@ -320,7 +323,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( .toSet() } - Timber.i( + TangemLogger.i( "Successfully fetched staking balances for $userWalletId:\n${yieldBalances.joinToString("\n")}", ) stakeKitBalancesStore.storeActual(userWalletId = userWalletId, values = yieldBalances) @@ -337,7 +340,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( } }, onError = { throwable -> - Timber.e(throwable, "Unable to fetch staking balances $userWalletId") + TangemLogger.e("Unable to fetch staking balances $userWalletId", throwable) stakeKitBalancesStore.storeError(userWalletId = userWalletId, stakingIds = stakingIds) diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducer.kt index bf66f62a4a..5f96221f71 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducer.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducer.kt @@ -10,13 +10,13 @@ import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier import com.tangem.domain.staking.single.SingleStakingBalanceProducer import com.tangem.domain.staking.single.SingleStakingBalanceProducer.Companion.selectStakingBalance import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import timber.log.Timber /** * Default implementation of [SingleStakingBalanceProducer] @@ -39,7 +39,7 @@ internal class DefaultSingleStakingBalanceProducer @AssistedInject constructor( override val fallback: Option = StakingBalance.Error(stakingId = params.stakingId).some() override fun produce(): Flow { - Timber.i("Producing staking balance for params:\n$params") + TangemLogger.i("Producing staking balance for params:\n$params") return multiStakingBalanceSupplier( params = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId), diff --git a/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt b/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt index 0fe16ead81..696578c106 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/utils/DefaultStakingCleaner.kt @@ -9,10 +9,10 @@ import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.utils.StakingCleaner import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext -import timber.log.Timber /** * Default implementation of [StakingCleaner]. @@ -32,7 +32,7 @@ internal class DefaultStakingCleaner( override suspend fun invoke(userWalletId: UserWalletId, currencies: List) { if (currencies.isEmpty()) { - Timber.d("No currencies to clear for wallet: $userWalletId") + TangemLogger.d("No currencies to clear for wallet: $userWalletId") return } @@ -41,7 +41,7 @@ internal class DefaultStakingCleaner( } if (stakingIds.isEmpty()) { - Timber.d("All currencies have no stakingIds to clear for wallet: $userWalletId") + TangemLogger.d("All currencies have no stakingIds to clear for wallet: $userWalletId") return } @@ -50,7 +50,7 @@ internal class DefaultStakingCleaner( override suspend fun invoke(userWalletId: UserWalletId, stakingIds: Set) { if (stakingIds.isEmpty()) { - Timber.d("No stakingIds to clear for wallet: $userWalletId") + TangemLogger.d("No stakingIds to clear for wallet: $userWalletId") return } @@ -66,13 +66,13 @@ internal class DefaultStakingCleaner( runSuspendCatching { stakeKitBalancesStore.clear(userWalletId, stakingIds) } - .onFailure { Timber.e(it, "Failed to clear StakeKit balance statuses for wallet: $userWalletId") } + .onFailure { TangemLogger.e("Failed to clear StakeKit balance statuses for wallet: $userWalletId", it) } } private suspend fun clearP2PEthPoolBalancesStore(userWalletId: UserWalletId, stakingIds: Set) { runSuspendCatching { p2pEthPoolBalancesStore.clear(userWalletId, stakingIds) } - .onFailure { Timber.e(it, "Failed to clear P2PEthPool balance statuses for wallet: $userWalletId") } + .onFailure { TangemLogger.e("Failed to clear P2PEthPool balance statuses for wallet: $userWalletId", it) } } } \ No newline at end of file diff --git a/data/swap/build.gradle.kts b/data/swap/build.gradle.kts index a7ea5bd55c..8b9fdaffe8 100644 --- a/data/swap/build.gradle.kts +++ b/data/swap/build.gradle.kts @@ -52,7 +52,6 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index e4bbdeebee..0f4ef33398 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -35,11 +35,11 @@ import com.tangem.domain.swap.models.* import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext -import timber.log.Timber import java.io.IOException import java.math.BigDecimal import java.util.UUID @@ -392,7 +392,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( ).getOrThrow() }, onError = { error -> - Timber.w(error, "Unable to get pairs") + TangemLogger.w("Unable to get pairs", error) throw error }, ) @@ -438,7 +438,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( return try { txDetailsMoshiAdapter.fromJson(txDetailsJson) } catch (e: IOException) { - Timber.e(e, "error parsing txDetailsJson") + TangemLogger.e("error parsing txDetailsJson", e) null } } diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index 96158e5dfb..f25b6ec88e 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -72,7 +72,6 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.moshi.kotlin) implementation(deps.retrofit) // For HttpException - implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) // endregion diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CustomTokensMerger.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CustomTokensMerger.kt index 4d0cf9c1fc..fe43c7fe58 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CustomTokensMerger.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CustomTokensMerger.kt @@ -8,10 +8,10 @@ import com.tangem.datasource.api.tangemTech.models.CoinsResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext -import timber.log.Timber /** * Responsible for merging custom tokens into a user's token response. @@ -105,7 +105,7 @@ internal class CustomTokensMerger( ).bind() }, onError = { error -> - Timber.e(error, "Unable to fetch token:\n$token") + TangemLogger.e("Unable to fetch token:\n$token", error) null }, ) diff --git a/data/transaction/build.gradle.kts b/data/transaction/build.gradle.kts index 7becb2b968..a372c747a8 100644 --- a/data/transaction/build.gradle.kts +++ b/data/transaction/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { kapt(deps.hilt.kapt) /** Other */ - implementation(deps.timber) /** tests */ testImplementation(projects.common.test) 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 06b461cbd4..ba4141909d 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 @@ -33,8 +33,8 @@ import com.tangem.domain.transaction.models.EventTransactionTypeDto import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber import java.math.BigDecimal import java.math.BigInteger @@ -271,7 +271,7 @@ internal class DefaultTransactionRepository( Result.failure(ex) } } else { - Timber.e("${walletManager?.wallet?.blockchain} does not support transaction validation") + TangemLogger.e("${walletManager?.wallet?.blockchain} does not support transaction validation") Result.success(Unit) } } @@ -441,14 +441,14 @@ internal class DefaultTransactionRepository( val response = tangemTechApi.transactionEvents(body) response.fold( onSuccess = { - Timber.d("Successfully sent yield supply transaction hash: $hash") + TangemLogger.d("Successfully sent yield supply transaction hash: $hash") }, onError = { error -> - Timber.e(error, "Failed to send yield supply transaction hash: $hash") + TangemLogger.e("Failed to send yield supply transaction hash: $hash", error) }, ) }.onFailure { error -> - Timber.e(error, "Failed to send yield supply transaction hash: $hash") + TangemLogger.e("Failed to send yield supply transaction hash: $hash", error) } } @@ -460,7 +460,7 @@ internal class DefaultTransactionRepository( derivationPath = network.derivationPath.value, ) val preparer = walletManager as? TransactionPreparer ?: run { - Timber.e("${walletManager?.wallet?.blockchain} does not support TransactionBuilder") + TangemLogger.e("${walletManager?.wallet?.blockchain} does not support TransactionBuilder") error("Wallet manager does not support TransactionPreparer") } return preparer diff --git a/data/txhistory/build.gradle.kts b/data/txhistory/build.gradle.kts index 78b3545254..17ca52c9ce 100644 --- a/data/txhistory/build.gradle.kts +++ b/data/txhistory/build.gradle.kts @@ -29,7 +29,6 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.androidx.paging.runtime) - implementation(deps.timber) implementation(deps.jodatime) implementation(tangemDeps.blockchain) diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index 23761f8743..1763c2dd4b 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -20,9 +20,9 @@ import com.tangem.domain.txhistory.repository.TxHistoryRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.utils.SdkPageConverter import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext -import timber.log.Timber class DefaultTxHistoryRepository( private val cacheRegistry: CacheRegistry, @@ -110,7 +110,7 @@ class DefaultTxHistoryRepository( ) ?.items.orEmpty() } catch (e: Throwable) { - Timber.e(e, "Unable to load the transaction history for the requested page: ${Page.Initial}") + TangemLogger.e("Unable to load the transaction history for the requested page: ${Page.Initial}", e) emptyList() } } diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/RefactoredTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/RefactoredTxHistoryRepository.kt index 972344ad6e..88b9b82391 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/RefactoredTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/RefactoredTxHistoryRepository.kt @@ -16,7 +16,7 @@ import com.tangem.pagination.BatchFetchResult import com.tangem.pagination.BatchListSource import com.tangem.pagination.toBatchFlow import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class RefactoredTxHistoryRepository( private val walletManagersFacade: WalletManagersFacade, @@ -96,16 +96,17 @@ internal class RefactoredTxHistoryRepository( .filterIfTxAlreadyAdded(apiItems = items) return if (recentItems.isEmpty()) { - Timber.d("Nothing to add to TxHistory") + TangemLogger.d("Nothing to add to TxHistory") this } else { - Timber.d( - "Recent transactions were added to TxHistory: %s", - recentItems.joinToString( - prefix = "[", - postfix = "]", - transform = TxInfo::txHash, - ), + TangemLogger.d( + "Recent transactions were added to TxHistory: ${ + recentItems.joinToString( + prefix = "[", + postfix = "]", + transform = TxInfo::txHash, + ) + }", ) return copy(items = recentItems + items) diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt index 2f67ef406b..c95c0761bb 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/paging/TxHistoryPagingSource.kt @@ -11,7 +11,7 @@ import com.tangem.domain.txhistory.models.Page import com.tangem.domain.txhistory.models.PaginationWrapper import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.utils.SdkPageConverter -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class TxHistoryPagingSource( private val sourceParams: Params, @@ -44,7 +44,7 @@ internal class TxHistoryPagingSource( } LoadResult.Page(wrappedItems.items, prevKey = null, nextKey = nextKey) } catch (e: Throwable) { - Timber.e(e, "Unable to load the transaction history for the requested page: $pageToLoad") + TangemLogger.e("Unable to load the transaction history for the requested page: $pageToLoad", e) LoadResult.Error(e) } @@ -89,16 +89,17 @@ internal class TxHistoryPagingSource( .filterIfTxAlreadyAdded(apiItems = items) return if (recentItems.isEmpty()) { - Timber.d("Nothing to add to TxHistory") + TangemLogger.d("Nothing to add to TxHistory") this } else { - Timber.d( - "Recent transactions were added to TxHistory: %s", - recentItems.joinToString( - prefix = "[", - postfix = "]", - transform = TxInfo::txHash, - ), + TangemLogger.d( + "Recent transactions were added to TxHistory: ${ + recentItems.joinToString( + prefix = "[", + postfix = "]", + transform = TxInfo::txHash, + ) + }", ) return copy(items = recentItems + items) diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index ef2ebc1613..80f1139140 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -54,7 +54,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.arrow.fx) implementation(deps.jodatime) - implementation(deps.timber) implementation(deps.androidx.paging.runtime) implementation(deps.moshi.kotlin) ksp(deps.moshi.kotlin.codegen) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt index 505383a099..6a903986b1 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt @@ -11,7 +11,7 @@ import com.tangem.data.pay.util.TangemPayErrorConverter import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val TAG = "TangemPay: DefaultTangemPayCryptoCurrencyFactory" @@ -53,7 +53,7 @@ internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor( decimals = TOKEN_DECIMALS, ) }.mapLeft { exception -> - Timber.tag(TAG).e(exception) + TangemLogger.withTag(TAG).e("Error", exception) errorConverter.convert(exception) } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index fe1aa3f0bd..7342c3da2a 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -16,7 +16,7 @@ import com.tangem.domain.visa.error.VisaApiError import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.security.isSecurityExposed import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val TAG = "PaymentAccountStatusFetcher" @@ -31,12 +31,14 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( override suspend fun invoke(params: PaymentAccountStatusFetcher.Params): Either = eitherOn(dispatchers.default) { - Timber.tag(TAG).i("fetch: ${params.userWalletId.stringValue}") + TangemLogger.withTag(TAG).i("fetch: ${params.userWalletId.stringValue}") if (deviceSecurity.isSecurityExposed()) { - Timber.tag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") - Timber.tag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") - Timber.tag(TAG).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") + TangemLogger.withTag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") + TangemLogger.withTag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") + TangemLogger.withTag( + TAG, + ).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") return@eitherOn paymentAccountStatusesStore.store( userWalletId = params.userWalletId, @@ -47,7 +49,9 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( val status = onboardingRepository.hasTangemPayInWallet(userWalletId = params.userWalletId) .fold( ifLeft = { error -> - Timber.tag(TAG).e("Failed check wallet ${params.userWalletId}: ${error.javaClass.simpleName}") + TangemLogger.withTag( + TAG, + ).e("Failed check wallet ${params.userWalletId}: ${error.javaClass.simpleName}") when (error) { is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) @@ -57,7 +61,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( proceedHasTangemPayResult(userWalletId = params.userWalletId, hasTangemPay = hasTangemPay) }, ) - Timber.tag(TAG).i("invoke status ${params.userWalletId}: $status") + TangemLogger.withTag(TAG).i("invoke status ${params.userWalletId}: $status") paymentAccountStatusesStore.store(userWalletId = params.userWalletId, status = status) } @@ -65,7 +69,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( userWalletId: UserWalletId, hasTangemPay: Boolean, ): PaymentAccountStatus { - Timber.tag(TAG).i("proceedHasTangemPayResult for $userWalletId hasTangemPay: $hasTangemPay") + TangemLogger.withTag(TAG).i("proceedHasTangemPayResult for $userWalletId hasTangemPay: $hasTangemPay") return if (hasTangemPay) { fetchTangemPayAccountStatus(userWalletId = userWalletId) } else { @@ -98,15 +102,15 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( private suspend fun proceedWithoutOrder(userWalletId: UserWalletId): PaymentAccountStatus { return onboardingRepository.getCustomerInfo(userWalletId).fold( ifLeft = { error -> - Timber.tag(TAG).e("proceedWithoutOrder $userWalletId error: $error") + TangemLogger.withTag(TAG).e("proceedWithoutOrder $userWalletId error: $error") error.mapToPaymentAccountStatus() }, ifRight = { customerInfo -> - Timber.tag(TAG).i("proceedWithoutOrder data customerInfo $userWalletId") + TangemLogger.withTag(TAG).i("proceedWithoutOrder data customerInfo $userWalletId") val status = customerInfo.mapToPaymentAccountStatus() if (customerInfo.productInstance == null) { onboardingRepository.createOrder(userWalletId) - .onLeft { Timber.tag(TAG).e("createOrder failed: $it") } + .onLeft { TangemLogger.withTag(TAG).e("createOrder failed: $it") } } status }, @@ -116,11 +120,11 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( private suspend fun proceedWithOrderId(userWalletId: UserWalletId, orderId: String): PaymentAccountStatus { return customerOrderRepository.getOrderData(userWalletId, orderId = orderId).fold( ifLeft = { error -> - Timber.tag(TAG).e("proceedWithOrderId $userWalletId orderId: $orderId error: $error") + TangemLogger.withTag(TAG).e("proceedWithOrderId $userWalletId orderId: $orderId error: $error") error.mapToPaymentAccountStatus() }, ifRight = { orderData -> - Timber.tag(TAG).i("proceedWithOrderId $userWalletId: $orderId status: ${orderData.status}") + TangemLogger.withTag(TAG).i("proceedWithOrderId $userWalletId: $orderId status: ${orderData.status}") when (orderData.status) { // Kyc is passed and user waits for order creation -> no need to get customer info OrderStatus.NEW, diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt index 0c0b28df99..2f05b72de7 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt @@ -19,18 +19,21 @@ import com.tangem.datasource.api.pay.models.response.FreezeUnfreezeCardResponse import com.tangem.datasource.api.pay.models.response.OrderResponse.Result.Status import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayStorage -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.SetPinResult import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayCardDetails import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.visa.model.TangemPayCardFrozenState -import kotlinx.coroutines.* +import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import timber.log.Timber import javax.inject.Inject import kotlin.time.Duration.Companion.seconds @@ -296,7 +299,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( cardFrozenStateStore.store(cardId, finalState) } }.onLeft { error -> - Timber.e("error ${error.errorCode}") + TangemLogger.e("error ${error.errorCode}") // stop retrying after 3 errors if (retryCount > MAX_POLLING_RETRIES) { pollingJobs.remove(key = orderId) @@ -305,7 +308,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( retryCount++ } } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) storePollingMutex.withLock { pollingJobs.remove(orderId) } @@ -338,7 +341,7 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( } private fun catchException(throwable: Throwable): Either { - Timber.tag(TAG).e(throwable) + TangemLogger.withTag(TAG).e("Error", throwable) return errorConverter.convert(throwable).left() } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt index 555c6c8e32..74512256e4 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt @@ -24,13 +24,13 @@ import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.extensions.addHexPrefix +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode import java.util.Currency @@ -176,14 +176,14 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( } } if (attemptCount >= MAX_POLLING_ATTEMPTS) { - Timber.tag(TAG).e("Polling stopped after $attemptCount unsuccessful attempts") + TangemLogger.withTag(TAG).e("Polling stopped after $attemptCount unsuccessful attempts") tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) pollingMutex.withLock { pollingJobs.remove(key) } } } catch (exception: CancellationException) { throw exception } catch (exception: Exception) { - Timber.tag(TAG).e(exception) + TangemLogger.withTag(TAG).e("Error", exception) tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) pollingMutex.withLock { pollingJobs.remove(key) } } @@ -240,7 +240,7 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( } catch (exception: CancellationException) { throw exception } catch (exception: Exception) { - Timber.tag(TAG).e(exception) + TangemLogger.withTag(TAG).e("Error", exception) } } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt index 3e83c4ff0d..c29153ce52 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/TangemPayRequestPerformer.kt @@ -17,10 +17,10 @@ import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.domain.visa.model.getAuthHeader import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import timber.log.Timber import java.util.UUID import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -69,7 +69,7 @@ internal class TangemPayRequestPerformer @Inject constructor( } }, catch = { errorConverter.convert(it).left() }, - ).onLeft { visaApiError -> Timber.tag(TAG).e(visaApiError.toString()) } + ).onLeft { visaApiError -> TangemLogger.withTag(TAG).e(visaApiError.toString()) } } suspend fun getCustomerWalletAddress(userWalletId: UserWalletId): String { @@ -129,7 +129,7 @@ internal class TangemPayRequestPerformer @Inject constructor( idempotencyKey = UUID.randomUUID().toString(), ) }.mapLeft { error -> - Timber.tag(TAG).e("Can not refresh auth tokens: $error") + TangemLogger.withTag(TAG).e("Can not refresh auth tokens: $error") if (error is VisaApiError.ServerUnavailable) error else VisaApiError.RefreshTokenExpired }.onRight { tokens -> tangemPayStorage.storeAuthTokens(customerWalletAddress = customerWalletAddress, tokens = tokens) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt index 4eec265d04..d42e7623a1 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt @@ -4,15 +4,15 @@ import androidx.datastore.core.DataStore import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch -import timber.log.Timber internal typealias WalletIdWithPaymentStatus = Map internal typealias WalletIdWithPaymentStatusDM = Map @@ -39,7 +39,7 @@ internal class PaymentAccountStatusesStore( }, ) } catch (e: Exception) { - Timber.e(e, "Error while loading cached payment account statuses") + TangemLogger.e("Error while loading cached payment account statuses", e) } } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt index be1d69448d..c75c111c9d 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/TangemPayTxHistoryItemConverter.kt @@ -7,9 +7,9 @@ import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.isPositive import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.orZero +import com.tangem.utils.logging.TangemLogger import org.joda.time.DateTime import org.joda.time.DateTimeZone -import timber.log.Timber import java.util.Currency internal class TangemPayTxHistoryItemConverter(moshi: Moshi) : @@ -26,7 +26,7 @@ internal class TangemPayTxHistoryItemConverter(moshi: Moshi) : ?: value.fee?.let { convertFee(id = value.id, fee = it) } ?: value.collateral?.let { convertCollateral(id = value.id, collateral = it) } ?: run { - Timber.wtf("unknown type of transaction: $value") + TangemLogger.e("unknown type of transaction: $value") null } } @@ -82,7 +82,7 @@ internal class TangemPayTxHistoryItemConverter(moshi: Moshi) : collateral: TangemPayTxHistoryResponse.Collateral, ): TangemPayTxHistoryItem.Collateral? { val date = collateral.postedAt ?: return run { - Timber.e("Collateral transaction postedAt is null: $collateral") + TangemLogger.e("Collateral transaction postedAt is null: $collateral") return@run null } return TangemPayTxHistoryItem.Collateral( diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt index 07ebc8a190..df5b49ac6c 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaTxHistoryPagingSource.kt @@ -7,10 +7,10 @@ import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.visa.model.VisaTxHistoryItem import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext -import timber.log.Timber internal class VisaTxHistoryPagingSource( params: Params, @@ -56,7 +56,7 @@ internal class VisaTxHistoryPagingSource( LoadResult.Page(items, prevOffset, nextOffset) } catch (e: Throwable) { - Timber.e(e, "Unable to load the transaction history for the requested offset: $offsetToLoad") + TangemLogger.e("Unable to load the transaction history for the requested offset: $offsetToLoad", e) LoadResult.Error(e) } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt index b35c96e84d..13522ffe02 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/initialize/DefaultWcInitializeUseCase.kt @@ -9,10 +9,10 @@ import com.reown.walletkit.client.WalletKit import com.tangem.data.walletconnect.pair.WcPairSdkDelegate import com.tangem.data.walletconnect.request.DefaultWcRequestService import com.tangem.data.walletconnect.sessions.DefaultWcSessionsManager -import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.data.walletconnect.utils.WC_TAG +import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultWcInitializeUseCase( private val application: Application, @@ -48,7 +48,7 @@ internal class DefaultWcInitializeUseCase( application = application, metaData = appMetaData, ) { error -> - Timber.tag(WC_TAG).e("Error while initializing client: $error") + TangemLogger.withTag(WC_TAG).e("Error while initializing client: $error") } WalletKit.initialize( @@ -57,10 +57,10 @@ internal class DefaultWcInitializeUseCase( val walletDelegate = defineWalletDelegate() WalletKit.setWalletDelegate(walletDelegate) wcSdkObservers.forEach { it.onWcSdkInit() } - Timber.tag(WC_TAG).i("onWcSdkInit") + TangemLogger.withTag(WC_TAG).i("onWcSdkInit") }, onError = { error -> - Timber.tag(WC_TAG).e("Error while initializing Web3Wallet: $error") + TangemLogger.withTag(WC_TAG).e("Error while initializing Web3Wallet: $error") }, ) } @@ -70,32 +70,32 @@ internal class DefaultWcInitializeUseCase( get() = super.onSessionAuthenticate override fun onConnectionStateChange(state: Wallet.Model.ConnectionState) { - Timber.tag(WC_TAG).i("sdk callback onConnectionStateChange isAvailable=${state.isAvailable}") + TangemLogger.withTag(WC_TAG).i("sdk callback onConnectionStateChange isAvailable=${state.isAvailable}") wcSdkObservers.forEach { it.onConnectionStateChange(state) } } override fun onError(error: Wallet.Model.Error) { - Timber.tag(WC_TAG).e(error.throwable, "sdk callback onError") + TangemLogger.withTag(WC_TAG).e("sdk callback onError", error.throwable) wcSdkObservers.forEach { it.onError(error) } } override fun onProposalExpired(proposal: Wallet.Model.ExpiredProposal) { - Timber.tag(WC_TAG).i("sdk callback onProposalExpired $proposal") + TangemLogger.withTag(WC_TAG).i("sdk callback onProposalExpired $proposal") wcSdkObservers.forEach { it.onProposalExpired(proposal) } } override fun onRequestExpired(request: Wallet.Model.ExpiredRequest) { - Timber.tag(WC_TAG).i("sdk callback onRequestExpired $request") + TangemLogger.withTag(WC_TAG).i("sdk callback onRequestExpired $request") wcSdkObservers.forEach { it.onRequestExpired(request) } } override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { - Timber.tag(WC_TAG).i("sdk callback onSessionDelete $sessionDelete") + TangemLogger.withTag(WC_TAG).i("sdk callback onSessionDelete $sessionDelete") wcSdkObservers.forEach { it.onSessionDelete(sessionDelete) } } override fun onSessionExtend(session: Wallet.Model.Session) { - Timber.tag(WC_TAG).i("sdk callback onSessionExtend $session") + TangemLogger.withTag(WC_TAG).i("sdk callback onSessionExtend $session") wcSdkObservers.forEach { it.onSessionExtend(session) } } @@ -103,7 +103,7 @@ internal class DefaultWcInitializeUseCase( sessionProposal: Wallet.Model.SessionProposal, verifyContext: Wallet.Model.VerifyContext, ) { - Timber.tag(WC_TAG).i("sdk callback onSessionProposal $sessionProposal") + TangemLogger.withTag(WC_TAG).i("sdk callback onSessionProposal $sessionProposal") wcSdkObservers.forEach { it.onSessionProposal(sessionProposal, verifyContext) } } @@ -111,17 +111,17 @@ internal class DefaultWcInitializeUseCase( sessionRequest: Wallet.Model.SessionRequest, verifyContext: Wallet.Model.VerifyContext, ) { - Timber.tag(WC_TAG).i("sdk callback onSessionRequest $sessionRequest") + TangemLogger.withTag(WC_TAG).i("sdk callback onSessionRequest $sessionRequest") wcSdkObservers.forEach { it.onSessionRequest(sessionRequest, verifyContext) } } override fun onSessionSettleResponse(settleSessionResponse: Wallet.Model.SettledSessionResponse) { - Timber.tag(WC_TAG).i("sdk callback onSessionSettleResponse $settleSessionResponse") + TangemLogger.withTag(WC_TAG).i("sdk callback onSessionSettleResponse $settleSessionResponse") wcSdkObservers.forEach { it.onSessionSettleResponse(settleSessionResponse) } } override fun onSessionUpdateResponse(sessionUpdateResponse: Wallet.Model.SessionUpdateResponse) { - Timber.tag(WC_TAG).i("sdk callback onSessionUpdateResponse $sessionUpdateResponse") + TangemLogger.withTag(WC_TAG).i("sdk callback onSessionUpdateResponse $sessionUpdateResponse") wcSdkObservers.forEach { it.onSessionUpdateResponse(sessionUpdateResponse) } } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/SolanaBlockAidAddressConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/SolanaBlockAidAddressConverter.kt index 312d752068..6347e2f390 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/SolanaBlockAidAddressConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/SolanaBlockAidAddressConverter.kt @@ -1,17 +1,17 @@ package com.tangem.data.walletconnect.network.solana import com.tangem.blockchain.extensions.decodeBase58 -import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.blockchain.extensions.encodeBase64NoWrap +import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.utils.converter.Converter -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject internal class SolanaBlockAidAddressConverter @Inject constructor() : Converter { override fun convert(value: String): String? { return value.decodeBase58()?.encodeBase64NoWrap() ?: run { - Timber.tag(WC_TAG).e("Error while converting Solana transaction account address") + TangemLogger.withTag(WC_TAG).e("Error while converting Solana transaction account address") null } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt index c0d665b854..157cfaf912 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaSignTransactionUseCase.kt @@ -22,13 +22,13 @@ import com.tangem.domain.walletconnect.usecase.method.SignRequirements import com.tangem.domain.walletconnect.usecase.method.WcSignState import com.tangem.domain.walletconnect.usecase.method.WcTransactionUseCase import com.tangem.lib.crypto.BlockchainUtils.SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import okio.ByteString.Companion.decodeBase64 -import timber.log.Timber @Suppress("LongParameterList") internal class WcSolanaSignTransactionUseCase @AssistedInject constructor( @@ -57,13 +57,13 @@ internal class WcSolanaSignTransactionUseCase @AssistedInject constructor( val formattedHash = getFormattedHash(hash) // uses for flow sendLargeSolanaTransaction if (context.session.wallet is UserWallet.Cold && isLargeHash(formattedHash)) { // workaround for large transactions that cannot be signed directly by card - Timber.w("The transaction hash is too large to be signed directly: ${formattedHash.size} bytes") + TangemLogger.w("The transaction hash is too large to be signed directly: ${formattedHash.size} bytes") sendLargeSolanaTransactionUseCase(context.session.wallet as UserWallet.Cold, context.network, formattedHash) .fold( - ifLeft = { + ifLeft = { error -> analytics.send(SolanaLargeTransactionStatus(SolanaLargeTransactionStatus.Status.Failed)) - Timber.e(it.toString()) - emit(state.toResult(parseSendError(it).left())) + TangemLogger.e(error.toString()) + emit(state.toResult(parseSendError(error).left())) }, ifRight = { analytics.send(SolanaLargeTransactionStatus(SolanaLargeTransactionStatus.Status.Success)) @@ -115,7 +115,7 @@ internal class WcSolanaSignTransactionUseCase @AssistedInject constructor( return try { SolanaTransactionHelper.removeSignaturesPlaceholders(hash) } catch (e: Exception) { - Timber.e("Failed to format the hash: ${e.message}") + TangemLogger.e("Failed to format the hash: ${e.message}") hash } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index 78d58bd321..02ce4d738f 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -16,6 +16,7 @@ import com.tangem.domain.walletconnect.model.* import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.usecase.pair.WcPairState import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -24,7 +25,6 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import org.joda.time.DateTime import org.joda.time.Duration -import timber.log.Timber import java.net.URI @Suppress("LongParameterList") @@ -42,7 +42,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( @Suppress("LongMethod") override operator fun invoke(): Flow { return flow { - Timber.tag(WC_TAG).i("start pair flow $pairRequest") + TangemLogger.withTag(WC_TAG).i("start pair flow $pairRequest") analytics.send( WcAnalyticEvents.NewPairInitiated( source = pairRequest.source, @@ -52,22 +52,22 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( emit(WcPairState.Loading) val pairResult = sdkDelegate.pair(pairRequest.uri) - .onLeft { - Timber.tag(WC_TAG).e(it, "Failed to call pair $pairRequest") + .onLeft { error -> + TangemLogger.withTag(WC_TAG).e("Failed to call pair $pairRequest", error) analytics.send( WcAnalyticEvents.PairFailed( - errorCode = it.code, - errorMessage = it.message, + errorCode = error.code, + errorMessage = error.message, ), ) - emit(WcPairState.Error(it)) + emit(WcPairState.Error(error)) } .getOrNull() ?: return@flow val (sdkSessionProposal, sdkVerifyContext) = pairResult // check unsupported dApps, just local constant for now, finish if unsupported if (UnsupportedDApps.list.any { sdkSessionProposal.url.contains(it, ignoreCase = true) }) { - Timber.tag(WC_TAG).i("Unsupported DApp ${sdkSessionProposal.name}") + TangemLogger.withTag(WC_TAG).i("Unsupported DApp ${sdkSessionProposal.name}") val error = WcPairState.Error(WcPairError.UnsupportedDApp(sdkSessionProposal.name)) emit(error) return@flow @@ -93,7 +93,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( emit(proposalState) // wait first terminal action and continue WC pair flow - Timber.tag(WC_TAG).i("pair wait terminal action ${sdkSessionProposal.name}") + TangemLogger.withTag(WC_TAG).i("pair wait terminal action ${sdkSessionProposal.name}") val terminalAction = onCallTerminalAction.receiveAsFlow().first() val sessionForApprove: WcSessionApprove? = when (terminalAction) { is TerminalAction.Approve -> terminalAction.sessionForApprove @@ -139,30 +139,30 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( ), ) proposalState.dAppSession.dAppMetaData - }.onLeft { + }.onLeft { error -> analytics.send( WcAnalyticEvents.DAppConnectionFailed( - errorCode = it.code, - errorMessage = it.message, + errorCode = error.code, + errorMessage = error.message, ), ) sdkDelegate.rejectSession(sdkSessionProposal.proposerPublicKey) - Timber.tag(WC_TAG).e(it, "Failed to approve session ${sdkSessionProposal.name}") + TangemLogger.withTag(WC_TAG).e("Failed to approve session ${sdkSessionProposal.name}", error) } emit(WcPairState.Approving.Result(sessionForApprove, either)) } - .catch { - val pairError: WcPairError = when (it) { - is TimeoutCancellationException -> WcPairError.TimeoutException(it.message.orEmpty()) - else -> WcPairError.Unknown(it.message.orEmpty()) + .catch { throwable -> + val pairError: WcPairError = when (throwable) { + is TimeoutCancellationException -> WcPairError.TimeoutException(throwable.message.orEmpty()) + else -> WcPairError.Unknown(throwable.message.orEmpty()) } emit(WcPairState.Error(pairError)) } - .onCompletion { - if (it != null) { - Timber.tag(WC_TAG).e(it, "Completed with error $pairRequest") + .onCompletion { throwable -> + if (throwable != null) { + TangemLogger.withTag(WC_TAG).e("Completed with error $pairRequest", throwable) } else { - Timber.tag(WC_TAG).i("Completed successfully $pairRequest") + TangemLogger.withTag(WC_TAG).i("Completed successfully $pairRequest") } } } @@ -191,7 +191,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( ) sdkDelegate.approve(pendingSessionForSave, sessionApprove) } catch (e: Throwable) { - Timber.tag(WC_TAG).e(e, "Failed to sdk approve session $pairRequest") + TangemLogger.withTag(WC_TAG).e("Failed to sdk approve session $pairRequest", e) WcPairError.ApprovalFailed(e.message.orEmpty()).left() } @@ -203,8 +203,8 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( val verificationInfo = when { verifyContext.validation == Wallet.Model.Validation.INVALID -> CheckDAppResult.UNSAFE verifyContext.isScam == true -> CheckDAppResult.UNSAFE - else -> blockAidVerifier.verifyDApp(DAppData(sessionProposal.url)).getOrElse { - Timber.tag(WC_TAG).e(it, "Failed to verify DApp ${sessionProposal.name}") + else -> blockAidVerifier.verifyDApp(DAppData(sessionProposal.url)).getOrElse { error -> + TangemLogger.withTag(WC_TAG).e("Failed to verify DApp ${sessionProposal.name}", error) CheckDAppResult.FAILED_TO_VERIFY } } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt index 926afee1d3..a747091d4a 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/WcPairSdkDelegate.kt @@ -7,17 +7,17 @@ import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.data.walletconnect.utils.WcSdkObserver -import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.data.walletconnect.utils.getDappOriginUrl +import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcPairError.ApprovalFailed import com.tangem.domain.walletconnect.model.WcPendingApprovalSessionDTO import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.* -import timber.log.Timber import kotlin.coroutines.resume import kotlin.time.Duration.Companion.seconds @@ -91,7 +91,7 @@ internal class WcPairSdkDelegate( } fun rejectSession(proposerPublicKey: String) { - Timber.tag(WC_TAG).i("reject session proposerPublicKey = $proposerPublicKey") + TangemLogger.withTag(WC_TAG).i("reject session proposerPublicKey = $proposerPublicKey") WalletKit.rejectSession( params = Wallet.Params.SessionReject( proposerPublicKey = proposerPublicKey, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt index b2393fe07f..130530c326 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestService.kt @@ -9,10 +9,10 @@ import com.tangem.data.walletconnect.utils.getDappOriginUrl import com.tangem.domain.walletconnect.WcRequestService import com.tangem.domain.walletconnect.model.WcMethodName import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import org.joda.time.DateTime -import timber.log.Timber internal class DefaultWcRequestService( private val requestConverters: Set, @@ -35,10 +35,10 @@ internal class DefaultWcRequestService( sessionRequest = sessionRequest, ), ) - Timber.tag(WC_TAG).i("handle request $sr") + TangemLogger.withTag(WC_TAG).i("handle request $sr") val name = requestConverters.firstNotNullOfOrNull { it.toWcMethodName(sr) } ?: WcMethodName.Unsupported(sr.request.method) - Timber.tag(WC_TAG).i("handle request name $name") + TangemLogger.withTag(WC_TAG).i("handle request name $name") if (name is WcMethodName.Unsupported) { respondService.rejectRequestNonBlock(sr) if (name.raw.startsWith("wallet_")) return @@ -56,7 +56,7 @@ internal class DefaultWcRequestService( val noHaveCached = cachedRequest.none { (_, hashParams) -> hash == hashParams } if (!noHaveCached) { - Timber.tag(WC_TAG).i("filter request $request") + TangemLogger.withTag(WC_TAG).i("filter request $request") } return noHaveCached } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestUseCaseFactory.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestUseCaseFactory.kt index 9745a0f84f..41caf2afe1 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestUseCaseFactory.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestUseCaseFactory.kt @@ -13,7 +13,7 @@ import com.tangem.domain.walletconnect.model.WcMethod import com.tangem.domain.walletconnect.model.WcRequestError.Companion.code import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject internal class DefaultWcRequestUseCaseFactory @Inject constructor( @@ -32,9 +32,9 @@ internal class DefaultWcRequestUseCaseFactory @Inject constructor( ?: HandleMethodError.UnknownError("Failed to create WcUseCase").left() val result = useCase.fold( - ifLeft = { - Timber.tag(WC_TAG).e("$it") - it.left() + ifLeft = { methodError -> + TangemLogger.withTag(WC_TAG).e("$methodError") + methodError.left() }, ifRight = { (it as? T)?.right() ?: HandleMethodError.Unsupported(WcMethod.Unsupported(request)).left() }, ) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt index ca0bc550dd..fdb3bf7bb1 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/respond/DefaultWcRespondService.kt @@ -10,11 +10,11 @@ import com.tangem.common.extensions.toHexString import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.domain.walletconnect.model.WcRequestError import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.suspendCancellableCoroutine import org.joda.time.Duration -import timber.log.Timber import kotlin.coroutines.resume internal class DefaultWcRespondService : WcRespondService { @@ -36,21 +36,21 @@ internal class DefaultWcRespondService : WcRespondService { result = response, ), ), - onSuccess = { + onSuccess = { requestResponse -> if (continuation.isCompleted) return@respondSessionRequest - val result = when (val response = it.jsonRpcResponse) { + val result = when (val response = requestResponse.jsonRpcResponse) { is Wallet.Model.JsonRpcResponse.JsonRpcError -> { - Timber.tag(WC_TAG).e("Failed respond $response for request $request") + TangemLogger.withTag(WC_TAG).e("Failed respond $response for request $request") WcRequestError.WcRespondError( code = response.code, message = response.message, ).left() } is Wallet.Model.JsonRpcResponse.JsonRpcResult -> { - Timber.tag(WC_TAG).i("Successful respond $response for request $request") + TangemLogger.withTag(WC_TAG).i("Successful respond $response for request $request") if (response.result == null) { - Timber.tag(WC_TAG).e( - "Response result is null, but it should be String. Casted to empty", + TangemLogger.withTag(WC_TAG).e( + "Response result is null, but requestResponse should be String. Casted to empty", ) } (response.result ?: "").right() @@ -58,16 +58,16 @@ internal class DefaultWcRespondService : WcRespondService { } continuation.resume(result) }, - onError = { + onError = { error -> if (continuation.isCompleted) return@respondSessionRequest - Timber.tag(WC_TAG).e(it.throwable, "Failed respond for request $request") - continuation.resume(WcRequestError.UnknownError(it.throwable).left()) + TangemLogger.withTag(WC_TAG).e("Failed respond for request $request", error.throwable) + continuation.resume(WcRequestError.UnknownError(error.throwable).left()) }, ) } override fun rejectRequestNonBlock(request: WcSdkSessionRequest, message: String) { - Timber.tag(WC_TAG).i("reject request $request") + TangemLogger.withTag(WC_TAG).i("reject request $request") removeCachedRequest(request) WalletKit.respondSessionRequest( params = Wallet.Params.SessionRequestResponse( diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index d27195854e..c6fed93088 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -23,13 +23,13 @@ import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext -import timber.log.Timber import kotlin.coroutines.resume @Suppress("LongParameterList") @@ -91,7 +91,7 @@ internal class DefaultWcSessionsManager( } override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { - Timber.i("onSessionDelete: $sessionDelete") + TangemLogger.i("onSessionDelete: $sessionDelete") onSessionDelete.trySend(sessionDelete) } @@ -150,7 +150,7 @@ internal class DefaultWcSessionsManager( val haveSomeUnknownSdkSessions = unknownSdkSessions.isNotEmpty() if (haveSomeUnknown) { - Timber.tag(WC_TAG).i("removeUnknownSessions $unknownStoredSessions") + TangemLogger.withTag(WC_TAG).i("removeUnknownSessions $unknownStoredSessions") store.removeSessions(unknownStoredSessions.toSet()) } @@ -162,7 +162,7 @@ internal class DefaultWcSessionsManager( val haveEmptyDto = emptyNetworksDto.isNotEmpty() if (haveEmptyDto) { - Timber.tag(WC_TAG).i("remove sessions without networks $emptyNetworksDto") + TangemLogger.withTag(WC_TAG).i("remove sessions without networks $emptyNetworksDto") store.removeSessions(emptyNetworksDto) } if (haveEmptySessions) { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt index 8bc9a0c492..f0d71c240c 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt @@ -11,8 +11,8 @@ import com.tangem.domain.walletconnect.model.WcMethod import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSolanaMethod import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.flow -import timber.log.Timber import javax.inject.Inject internal class BlockAidVerificationDelegate @Inject constructor( @@ -72,9 +72,9 @@ internal class BlockAidVerificationDelegate @Inject constructor( params = params, ), ).fold( - ifLeft = { - Timber.e("Failed to verify transaction: ${it.localizedMessage}") - emit(Lce.Error(it)) + ifLeft = { throwable -> + TangemLogger.e("Failed to verify transaction: ${throwable.localizedMessage}") + emit(Lce.Error(throwable)) }, ifRight = { emit(Lce.Content(it)) diff --git a/data/wallet-manager/build.gradle.kts b/data/wallet-manager/build.gradle.kts index e703847de0..ace6ee7b8a 100644 --- a/data/wallet-manager/build.gradle.kts +++ b/data/wallet-manager/build.gradle.kts @@ -43,7 +43,6 @@ dependencies { implementation(projects.libs.blockchainSdk) implementation(deps.androidx.datastore) implementation(deps.arrow.core) - implementation(deps.timber) /** Testing libraries */ testRuntimeOnly(deps.test.junit5.engine) diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt index 5791e4e9e0..68f31b8849 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt @@ -49,11 +49,11 @@ import com.tangem.domain.walletmanager.model.TokenInfo import com.tangem.domain.walletmanager.utils.SdkPageConverter import com.tangem.domain.wallets.extension.hasDerivation import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import timber.log.Timber import java.math.BigDecimal import java.util.EnumSet import java.util.concurrent.ConcurrentHashMap @@ -178,13 +178,13 @@ internal class DefaultWalletManagersFacade @Inject constructor( if (derivationPath != null && !userWallet.hasDerivation(blockchain, derivationPath) ) { - Timber.w("Derivation missed for: $blockchain") + TangemLogger.w("Derivation missed for: $blockchain") return UpdateWalletManagerResult.MissedDerivation } val walletManager = getOrCreateWalletManager(userWalletId, blockchain, derivationPath) if (walletManager == null || blockchain == Blockchain.Unknown) { - Timber.w("Unable to get a wallet manager for blockchain: $blockchain") + TangemLogger.w("Unable to get a wallet manager for blockchain: $blockchain") return UpdateWalletManagerResult.Unreachable() } @@ -285,7 +285,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( val gaslessFeeAddresses = try { gaslessTransactionRepository.getGaslessFeeAddresses() } catch (error: Throwable) { - Timber.e(error, "Failed to load gasless fee addresses; falling back to empty set") + TangemLogger.e("Failed to load gasless fee addresses; falling back to empty set", error) emptySet() } return when (itemsResult) { @@ -311,7 +311,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( extraTokens: Set, ): UpdateWalletManagerResult { if (derivationPath != null && !userWallet.hasDerivation(blockchain, derivationPath)) { - Timber.w("Derivation missed for: $blockchain") + TangemLogger.w("Derivation missed for: $blockchain") return UpdateWalletManagerResult.MissedDerivation } @@ -321,7 +321,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( derivationPath = derivationPath, ) if (walletManager == null || blockchain == Blockchain.Unknown) { - Timber.w("Unable to create or find a wallet manager for blockchain: $blockchain") + TangemLogger.w("Unable to create or find a wallet manager for blockchain: $blockchain") return UpdateWalletManagerResult.Unreachable() } @@ -360,7 +360,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( amountToCreateAccount = e.amountToCreateAccount, ) } catch (e: Throwable) { - Timber.w(e, "Unable to update a wallet manager for: ${walletManager.wallet.blockchain}") + TangemLogger.w("Unable to update a wallet manager for: ${walletManager.wallet.blockchain}", e) resultFactory.getUnreachableResult(walletManager) } @@ -376,7 +376,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( amountToCreateAccount = e.amountToCreateAccount, ) } catch (e: Throwable) { - Timber.w(e, "Unable to update a wallet manager for: ${walletManager.wallet.blockchain}") + TangemLogger.w("Unable to update a wallet manager for: ${walletManager.wallet.blockchain}", e) resultFactory.getUnreachableResult(walletManager) } @@ -584,7 +584,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = currency.network) if (walletManager == null) { - Timber.e("Unable to get a wallet manager for blockchain: ${currency.network.id}") + TangemLogger.e("Unable to get a wallet manager for blockchain: ${currency.network.id}") return emptyList() } diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/UpdateWalletManagerResultFactory.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/UpdateWalletManagerResultFactory.kt index a8d65edb01..c1ab2fb300 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/UpdateWalletManagerResultFactory.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/UpdateWalletManagerResultFactory.kt @@ -8,7 +8,7 @@ import com.tangem.data.walletmanager.utils.SdkAddressToAddressConverter import com.tangem.data.walletmanager.utils.TransactionDataToTxHistoryItemConverter import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.yield.supply.YieldSupplyStatus -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal /** Factory for creating [UpdateWalletManagerResult] */ @@ -68,7 +68,7 @@ internal class UpdateWalletManagerResultFactory { val amount = amountToCreateAccount ?: blockchain.amountToCreateAccount(walletManager, firstWalletToken) return if (amount == null) { - Timber.w("Unable to get required amount to create account for: $blockchain") + TangemLogger.w("Unable to get required amount to create account for: $blockchain") UpdateWalletManagerResult.Unreachable( selectedAddress = wallet.address, addresses = getAvailableAddresses(wallet.addresses), @@ -142,7 +142,7 @@ internal class UpdateWalletManagerResultFactory { val value = amount.value if (value == null) { - Timber.w("Currency amount must not be null: ${amount.currencySymbol}") + TangemLogger.w("Currency amount must not be null: ${amount.currencySymbol}") } return value diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt index edd6da8af5..e4b9650c5d 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/WalletManagerFactory.kt @@ -7,12 +7,12 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.walletmanager.extensions.makePublicKey import com.tangem.data.walletmanager.extensions.makeWalletManagerForApp -import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.config.curvesConfig +import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.wallets.derivations.derivationStyleProvider -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class WalletManagerFactory( private val blockchainSDKFactory: BlockchainSDKFactory, @@ -32,7 +32,7 @@ internal class WalletManagerFactory( derivationParams = derivationParams, ) } catch (e: Throwable) { - Timber.w(e, "Failed to create wallet manager for $blockchain") + TangemLogger.w("Failed to create wallet manager for $blockchain", e) null } } @@ -68,7 +68,7 @@ internal class WalletManagerFactory( ) } } catch (e: Throwable) { - Timber.w(e, "Failed to create wallet manager for $blockchain") + TangemLogger.w("Failed to create wallet manager for $blockchain", e) null } } diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt index 36016b121b..1247fce030 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/utils/TransactionDataToTxHistoryItemConverter.kt @@ -11,7 +11,7 @@ import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldS import com.tangem.blockchainsdk.models.UpdateWalletManagerResult.Address import com.tangem.domain.models.network.TxInfo import com.tangem.utils.converter.Converter -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal /** @@ -55,7 +55,7 @@ internal class TransactionDataToTxHistoryItemConverter( val value = amount.value if (value == null) { - Timber.w("Transaction amount must not be null: ${amount.currencySymbol}") + TangemLogger.w("Transaction amount must not be null: ${amount.currencySymbol}") } return when (feePaidCurrency) { diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index 512c474032..19a6351a4c 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -48,7 +48,6 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) implementation(deps.retrofit) - implementation(deps.timber) /** tests */ testImplementation(projects.test.core) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsPromoRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsPromoRepository.kt index e5fb0f7e81..22b82c3fb7 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsPromoRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsPromoRepository.kt @@ -14,8 +14,8 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.wallets.models.AppsFlyerConversionData import com.tangem.domain.wallets.repository.WalletsPromoRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber internal class DefaultWalletsPromoRepository( private val appPreferencesStore: AppPreferencesStore, @@ -47,7 +47,7 @@ internal class DefaultWalletsPromoRepository( if (savedBindingData != null) { bind(refcode = savedBindingData.refcode, campaign = savedBindingData.campaign) } else { - Timber.i("retryBindRefcodeWithWallets: Binding data isn't required") + TangemLogger.i("retryBindRefcodeWithWallets: Binding data isn't required") } } diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/cold/DefaultColdMapDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/cold/DefaultColdMapDerivationsRepository.kt index e00bde14e5..f3305bfb5e 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/cold/DefaultColdMapDerivationsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/cold/DefaultColdMapDerivationsRepository.kt @@ -20,8 +20,8 @@ import com.tangem.domain.wallets.usecase.BackendId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject private typealias DerivedKeys = Map @@ -60,14 +60,14 @@ internal class DefaultColdMapDerivationsRepository @Inject constructor( networks: List, ): UserWallet.Cold = withContext(dispatchers.io) { if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) { - Timber.d("Nothing to derive") + TangemLogger.d("Nothing to derive") return@withContext userWallet } val derivations = MissedDerivationsFinder(userWallet) .findByNetworks(networks) .ifEmpty { - Timber.d("Nothing to derive") + TangemLogger.d("Nothing to derive") return@withContext userWallet } diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt index 534b16cb36..c347bb1e54 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt @@ -16,8 +16,8 @@ import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository import com.tangem.domain.wallets.usecase.BackendId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject internal class DefaultDerivationsRepository @Inject constructor( @@ -29,7 +29,7 @@ internal class DefaultDerivationsRepository @Inject constructor( override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) { if (currencies.isEmpty()) { - Timber.d("Nothing to derive") + TangemLogger.d("Nothing to derive") return } diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt index 8d0f7ec5d1..bcf3ef947c 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt @@ -19,8 +19,8 @@ import com.tangem.domain.wallets.usecase.BackendId import com.tangem.hot.sdk.model.DeriveWalletRequest import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject internal class DefaultHotMapDerivationsRepository @Inject constructor( @@ -62,7 +62,7 @@ internal class DefaultHotMapDerivationsRepository @Inject constructor( val derivations = MissedDerivationsFinder(userWallet) .findByNetworks(networks) .ifEmpty { - Timber.d("Nothing to derive") + TangemLogger.d("Nothing to derive") return@withContext userWallet } diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt index 58df536d84..0e21f98e12 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/TangemHotWalletSigner.kt @@ -9,10 +9,10 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.hot.HotWalletAccessor import com.tangem.hot.sdk.model.DataToSign import com.tangem.operations.sign.SignData +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber class TangemHotWalletSigner @AssistedInject constructor( @Assisted private val userWallet: UserWallet.Hot, @@ -43,12 +43,12 @@ class TangemHotWalletSigner @AssistedInject constructor( ), ), ) - }.getOrElse { - Timber.e(it) - return if (it is TangemSdkError) { - CompletionResult.Failure(it) + }.getOrElse { throwable -> + TangemLogger.e("Error", throwable) + return if (throwable is TangemSdkError) { + CompletionResult.Failure(throwable) } else { - CompletionResult.Failure(TangemSdkError.ExceptionError(it)) + CompletionResult.Failure(TangemSdkError.ExceptionError(throwable)) } } @@ -76,12 +76,12 @@ class TangemHotWalletSigner @AssistedInject constructor( ) }, ) - }.getOrElse { - Timber.e(it) - return if (it is TangemSdkError) { - CompletionResult.Failure(it) + }.getOrElse { throwable -> + TangemLogger.e("Error", throwable) + return if (throwable is TangemSdkError) { + CompletionResult.Failure(throwable) } else { - CompletionResult.Failure(TangemSdkError.ExceptionError(it)) + CompletionResult.Failure(TangemSdkError.ExceptionError(throwable)) } } diff --git a/data/yield-supply/build.gradle.kts b/data/yield-supply/build.gradle.kts index cc7313cd67..d5edaba7cd 100644 --- a/data/yield-supply/build.gradle.kts +++ b/data/yield-supply/build.gradle.kts @@ -43,7 +43,6 @@ dependencies { kapt(deps.hilt.kapt) /** Other */ - implementation(deps.timber) /** tests */ testImplementation(projects.common.test) diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepository.kt index c761bd1160..19f06ecd0f 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepository.kt @@ -18,8 +18,8 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext -import timber.log.Timber import java.math.BigDecimal @Suppress("LargeClass") @@ -109,7 +109,7 @@ internal class DefaultYieldSupplyTransactionRepository( decimals = cryptoCurrency.decimals, ), ) - }.onFailure(Timber::e).getOrThrow() + }.onFailure { TangemLogger.e("Error", it) }.getOrThrow() } @Suppress("LongParameterList") @@ -202,7 +202,7 @@ internal class DefaultYieldSupplyTransactionRepository( derivationPath = cryptoCurrency.network.derivationPath.value, ) ?: error("Wallet manager not found") walletManager.calculateYieldModuleAddress() - }.onFailure(Timber::e).getOrThrow() + }.onFailure { TangemLogger.e("Error", it) }.getOrThrow() } override suspend fun getYieldContractAddress(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): String? = @@ -215,7 +215,7 @@ internal class DefaultYieldSupplyTransactionRepository( derivationPath = cryptoCurrency.network.derivationPath.value, ) ?: error("Wallet manager not found") walletManager.getYieldModuleAddress() - }.onFailure(Timber::e).getOrThrow() + }.onFailure { TangemLogger.e("Error", it) }.getOrThrow() } private suspend fun getYieldTokenStatus( @@ -249,7 +249,7 @@ internal class DefaultYieldSupplyTransactionRepository( isAllowedToSpend = isAllowedToSpend, effectiveProtocolBalance = protocolBalance, ) - }.onFailure(Timber::e).getOrNull() + }.onFailure { TangemLogger.e("Error", it) }.getOrNull() } private fun createDeployTransaction( diff --git a/domain/account/status/build.gradle.kts b/domain/account/status/build.gradle.kts index d22d8549d9..7c6c759fa8 100644 --- a/domain/account/status/build.gradle.kts +++ b/domain/account/status/build.gradle.kts @@ -33,11 +33,10 @@ dependencies { implementation(projects.libs.blockchainSdk) implementation(projects.libs.crypto) + implementation(projects.core.utils) implementation(deps.kotlin.datetime) implementation(deps.kotlin.serialization) - implementation(deps.timber) - implementation(deps.kermit) implementation(tangemDeps.blockchain) diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultFlowProducerTools.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultFlowProducerTools.kt index e42901b839..1b49f91932 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultFlowProducerTools.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultFlowProducerTools.kt @@ -1,12 +1,12 @@ package com.tangem.domain.account.status.producer -import co.touchlab.kermit.Logger import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.domain.core.flow.FlowProducer import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import javax.inject.Inject @@ -55,7 +55,7 @@ class DefaultFlowProducerTools @Inject constructor( private fun logError(cause: Throwable, flowProducerName: String, attempt: Long) { val tag = "FlowProducerRetryWhen" - Logger.withTag(tag) + TangemLogger.withTag(tag) .e("flowProducerName $flowProducerName attempt $attempt", cause) val event = ExceptionAnalyticsEvent( diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCase.kt index 55e21d936c..6b0553047f 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCase.kt @@ -14,8 +14,8 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.coroutineScope -import timber.log.Timber private typealias SortingErrorByAccountId = MutableMap @@ -68,7 +68,7 @@ class ApplyTokenListSortingUseCase( maybeSortedAccountList.toEitherNeg() .onLeft { errorByAccountId -> - Timber.e( + TangemLogger.e( """ Unable to sort tokens for accounts: ${ errorByAccountId.entries.joinToString { "${it.key.value}: ${it.value}" } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt index 71bf409b07..4043b8821f 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt @@ -20,7 +20,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract @@ -130,7 +130,7 @@ class GetAccountCurrencyByAddressUseCase( } return value ?: run { - Timber.d(message()) + TangemLogger.d(message()) raise(None) } } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt index 5bb104ed00..e9656e1601 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt @@ -8,7 +8,6 @@ import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher import com.tangem.domain.account.status.utils.CryptoCurrencyMetadataCleaner -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.core.utils.eitherOn import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.express.models.ExpressAsset @@ -22,10 +21,14 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.derivations.DerivationsRepository +import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching -import kotlinx.coroutines.* -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Use case for saving crypto currencies to a specific account. @@ -77,7 +80,7 @@ class ManageCryptoCurrenciesUseCase( skipDerivationErrors: Boolean = true, ): Either = eitherOn(dispatchers.default) { if (add.isEmpty() && remove.isEmpty()) { - Timber.d("No currencies to add or remove, skipping") + TangemLogger.d("No currencies to add or remove, skipping") return@eitherOn } @@ -89,7 +92,7 @@ class ManageCryptoCurrenciesUseCase( .modify(add = add, remove = remove) if (!modifiedCurrencyList.hasChanges) { - Timber.d("No changes in currencies, skipping") + TangemLogger.d("No changes in currencies, skipping") return@withContext } @@ -279,7 +282,7 @@ class ManageCryptoCurrenciesUseCase( createWalletManagers(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) } - .onFailure { Timber.e(it, "Failed to sync tokens for wallet $userWalletId") } + .onFailure { TangemLogger.e("Failed to sync tokens for wallet $userWalletId", it) } } /** @@ -296,7 +299,7 @@ class ManageCryptoCurrenciesUseCase( runSuspendCatching { walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network) } - .onFailure { Timber.e(it, "Failed to create wallet manager for network ${network.id}") } + .onFailure { TangemLogger.e("Failed to create wallet manager for network ${network.id}", it) } } } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt index bb277d0707..b0d767f38a 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyBalanceFetcher.kt @@ -5,11 +5,11 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.BalanceFetchingOperations import com.tangem.domain.tokens.FetchErrorFormatter import com.tangem.domain.tokens.FetchingSource +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import timber.log.Timber import java.util.concurrent.ConcurrentHashMap /** @@ -87,7 +87,7 @@ class CryptoCurrencyBalanceFetcher( ) if (errors.isNotEmpty()) { - Timber.e(FetchErrorFormatter.format(userWalletId, errors)) + TangemLogger.e(FetchErrorFormatter.format(userWalletId, errors)) } } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyOperations.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyOperations.kt index 63ff52a802..6acd7e70de 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyOperations.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/CryptoCurrencyOperations.kt @@ -11,7 +11,7 @@ import com.tangem.domain.account.status.utils.AccountCryptoCurrencyStatusFinder. import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Extension functions for retrieving [CryptoCurrency] from an [AccountList] or [Account.CryptoPortfolio]. @@ -57,7 +57,7 @@ object CryptoCurrencyOperations { val currencyId = catch( block = { CryptoCurrency.ID.fromValue(currencyIdValue) }, catch = { throwable -> - Timber.e("Error on converting currencyId: $throwable") + TangemLogger.e("Error on converting currencyId: $throwable") raise(None) }, ) diff --git a/domain/card/build.gradle.kts b/domain/card/build.gradle.kts index a7eff175d4..44e17e2ce6 100644 --- a/domain/card/build.gradle.kts +++ b/domain/card/build.gradle.kts @@ -23,8 +23,8 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.visa.models) + implementation(projects.core.utils) - implementation(deps.timber) implementation(tangemDeps.card.core) implementation(tangemDeps.blockchain) { diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/EdSingleCurrencyCardConfig.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/EdSingleCurrencyCardConfig.kt index 37ee742b77..f21ad15983 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/EdSingleCurrencyCardConfig.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/EdSingleCurrencyCardConfig.kt @@ -2,7 +2,7 @@ package com.tangem.domain.card.configs import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.EllipticCurve -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger object EdSingleCurrencyCardConfig : CardConfig { @@ -14,7 +14,7 @@ object EdSingleCurrencyCardConfig : CardConfig { EllipticCurve.Ed25519 } else -> { - Timber.e("Unsupported blockchain, curve not found") + TangemLogger.e("Unsupported blockchain, curve not found") null } } diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/GenericCardConfig.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/GenericCardConfig.kt index 099c93ed10..eb9870b4f6 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/GenericCardConfig.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/GenericCardConfig.kt @@ -2,7 +2,7 @@ package com.tangem.domain.card.configs import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.EllipticCurve -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger class GenericCardConfig(maxWalletCount: Int) : CardConfig { @@ -25,7 +25,7 @@ class GenericCardConfig(maxWalletCount: Int) : CardConfig { EllipticCurve.Ed25519 } else -> { - Timber.e("Unsupported blockchain, curve not found") + TangemLogger.e("Unsupported blockchain, curve not found") null } } diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/MultiWalletCardConfig.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/MultiWalletCardConfig.kt index 8e661fffee..5ce2718892 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/MultiWalletCardConfig.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/MultiWalletCardConfig.kt @@ -2,7 +2,7 @@ package com.tangem.domain.card.configs import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.EllipticCurve -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger object MultiWalletCardConfig : CardConfig { override val mandatoryCurves: List @@ -27,7 +27,7 @@ object MultiWalletCardConfig : CardConfig { EllipticCurve.Bls12381G2Aug } else -> { - Timber.e("Unsupported blockchain, curve not found") + TangemLogger.e("Unsupported blockchain, curve not found") null } } diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt index 0c4ba1c0b0..a545214f6f 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt @@ -2,7 +2,7 @@ package com.tangem.domain.card.configs import com.tangem.blockchain.common.Blockchain import com.tangem.common.card.EllipticCurve -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger data object Wallet2CardConfig : CardConfig { override val mandatoryCurves: List @@ -37,14 +37,14 @@ data object Wallet2CardConfig : CardConfig { // EllipticCurve.Ed25519 // } // else -> { - // Timber.e("Unsupported blockchain, curve not found") + // TangemLogger.e("Unsupported blockchain, curve not found") // null // } // } val curve = getPrimaryCurveForBlockchain(blockchain) // check curve supports if (!blockchain.getSupportedCurves().contains(curve)) { - Timber.e("Unsupported curve $curve for blockchain $blockchain") + TangemLogger.e("Unsupported curve $curve for blockchain $blockchain") return null } return curve diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index 1326610ec7..6d09dae413 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -43,7 +43,6 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) implementation(deps.reKotlin) - implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) /** Testing libraries */ diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index 7cf130837a..587e19d32d 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -22,7 +22,6 @@ dependencies { implementation(deps.kotlin.datetime) implementation(deps.kotlin.serialization) implementation(deps.jodatime) - implementation(deps.timber) implementation(projects.domain.legacy) implementation(projects.domain.walletManager) // TODO refactor to use from data module diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceProducer.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceProducer.kt index dc87e3b8ed..3a41d1eee8 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceProducer.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceProducer.kt @@ -7,7 +7,7 @@ import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.extensions.indexOfFirstOrNull -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Producer of staking balance for selected wallet [UserWalletId] @@ -51,9 +51,8 @@ interface SingleStakingBalanceProducer : FlowProducer { ), ) - Timber.e( - "Multiple balances found for staking ID $currentStakingId:\n%s", - currentBalances.joinToString("\n"), + TangemLogger.e( + "Multiple balances found for staking ID $currentStakingId:\n${currentBalances.joinToString("\n")}", ) val dataIndex = currentBalances.indexOfFirstOrNull { it is StakingBalance.Data } @@ -66,7 +65,7 @@ interface SingleStakingBalanceProducer : FlowProducer { } else { val balance = currentBalances.firstOrNull() ?: return null - Timber.i("Staking balance found for $currentStakingId:\n$balance") + TangemLogger.i("Staking balance found for $currentStakingId:\n$balance") balance } } diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 52ba7c414e..236ce7785f 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -53,7 +53,6 @@ dependencies { /** Android - Other */ implementation(deps.androidx.paging.runtime) - implementation(deps.timber) /** Utils */ implementation(deps.jodatime) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/BalanceFetchingOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/BalanceFetchingOperations.kt index 96df850a31..9538efb817 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/BalanceFetchingOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/BalanceFetchingOperations.kt @@ -8,10 +8,10 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope -import timber.log.Timber /** * Shared utility for fetching cryptocurrency balance data from multiple sources. @@ -116,14 +116,14 @@ class BalanceFetchingOperations( val stakingId = stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = currency) if (stakingId.isLeft { it is StakingIdFactory.Error.UnableToGetAddress }) { - Timber.e("Unable to get staking ID for user wallet $userWalletId and currency ${currency.id}") + TangemLogger.e("Unable to get staking ID for user wallet $userWalletId and currency ${currency.id}") } stakingId.getOrNull() } if (stakingIds.isEmpty()) { - Timber.i("No staking IDs found for user wallet $userWalletId") + TangemLogger.i("No staking IDs found for user wallet $userWalletId") return Unit.right() } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt index afb0384174..27a9af10ba 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt @@ -26,10 +26,10 @@ import com.tangem.domain.tokens.wallet.implementor.MultiWalletBalanceFetcher import com.tangem.domain.tokens.wallet.implementor.SingleWalletBalanceFetcher import com.tangem.domain.tokens.wallet.implementor.SingleWalletWithTokenBalanceFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope -import timber.log.Timber /** * Fetcher of wallet balance by [UserWalletId] @@ -184,7 +184,7 @@ class WalletBalanceFetcher internal constructor( check(errors.isEmpty()) { val message = FetchErrorFormatter.formatWalletErrors(userWalletId, errors) - Timber.e(message) + TangemLogger.e(message) message } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt index e7f95702ce..4bbf8f3557 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt @@ -2,14 +2,14 @@ package com.tangem.domain.tokens.wallet.implementor import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.tokens.FetchingSource import com.tangem.domain.tokens.MultiWalletAccountListFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.FetchingSource import com.tangem.domain.tokens.wallet.BaseWalletBalanceFetcher import com.tangem.domain.tokens.wallet.WalletFetchingSource +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.firstOrNull -import timber.log.Timber /** * Implementation of [BaseWalletBalanceFetcher] for MULTI-CURRENCY wallet @@ -37,7 +37,7 @@ internal class MultiWalletBalanceFetcher( multiWalletAccountListFetcher( params = MultiWalletAccountListFetcher.Params(userWalletId = userWalletId), ) - .onLeft(Timber::e) + .onLeft { TangemLogger.e("Error", it) } return multiWalletCryptoCurrenciesSupplier( params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId), diff --git a/domain/tokensync/build.gradle.kts b/domain/tokensync/build.gradle.kts index bdf6721531..35464995f3 100644 --- a/domain/tokensync/build.gradle.kts +++ b/domain/tokensync/build.gradle.kts @@ -16,5 +16,4 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) - implementation(deps.timber) } \ No newline at end of file diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt index d3f8fb0c31..ddf41c5794 100644 --- a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt +++ b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt @@ -6,9 +6,9 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokensync.repository.TokenSyncRepository import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import timber.log.Timber import java.util.concurrent.ConcurrentHashMap class SyncTokensUseCase( @@ -26,7 +26,7 @@ class SyncTokensUseCase( tokenSyncRepository.runSync(userWalletId) applyDiscoveredTokens(userWalletId) } catch (e: Exception) { - Timber.e(e, "Token sync failed for wallet: $userWalletId") + TangemLogger.e("Token sync failed for wallet: $userWalletId", e) } finally { activeSyncJobs.remove(userWalletId) } @@ -50,7 +50,7 @@ class SyncTokensUseCase( } } } catch (e: Exception) { - Timber.e(e, "Failed to apply pending syncs") + TangemLogger.e("Failed to apply pending syncs", e) } } } @@ -70,7 +70,7 @@ class SyncTokensUseCase( true }, ifLeft = { error -> - Timber.e("Failed to apply discovered tokens for wallet: $userWalletId, error: $error") + TangemLogger.e("Failed to apply discovered tokens for wallet: $userWalletId, error: $error") false }, ) diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index 3c13ebe644..ec2d1affc7 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -29,7 +29,6 @@ dependencies { implementation(deps.spongecastle.core) /** Libs - Other */ - implementation(deps.timber) implementation(deps.jodatime) implementation(deps.androidx.paging.runtime) implementation(deps.moshi) diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index da2687589a..511457acb6 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -12,8 +12,8 @@ import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.visa.error.VisaApiError import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.security.isSecurityExposed +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.* -import timber.log.Timber private const val TAG = "TangemPayMainScreenCustomerInfoUseCase" @@ -28,12 +28,14 @@ class TangemPayMainScreenCustomerInfoUseCase( field = MutableStateFlow(value = mapOf()) suspend fun fetch(userWalletId: UserWalletId) { - Timber.tag(TAG).i("fetch: ${userWalletId.stringValue}") + TangemLogger.withTag(TAG).i("fetch: ${userWalletId.stringValue}") if (deviceSecurity.isSecurityExposed()) { - Timber.tag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") - Timber.tag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") - Timber.tag(TAG).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") + TangemLogger.withTag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") + TangemLogger.withTag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") + TangemLogger.withTag( + TAG, + ).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") updateState(userWalletId = userWalletId, either = TangemPayCustomerInfoError.ExposedDeviceError.left()) return // fast exit @@ -42,7 +44,9 @@ class TangemPayMainScreenCustomerInfoUseCase( onboardingRepository.hasTangemPayInWallet(userWalletId) .fold( ifLeft = { error -> - Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}") + TangemLogger.withTag( + TAG, + ).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}") if (error is VisaApiError.NotPaeraCustomer) { showOnboardingBannerIfEligible(userWalletId) } else { @@ -50,7 +54,7 @@ class TangemPayMainScreenCustomerInfoUseCase( } }, ifRight = { hasTangemPay -> - Timber.tag(TAG).i("checkCustomerWallet for $userWalletId: $hasTangemPay") + TangemLogger.withTag(TAG).i("checkCustomerWallet for $userWalletId: $hasTangemPay") if (hasTangemPay) { val oldResult = state.value[userWalletId] if (oldResult == null) { @@ -125,14 +129,13 @@ class TangemPayMainScreenCustomerInfoUseCase( ): Either { return onboardingRepository.getCustomerInfo(userWalletId) .mapLeft { error -> - Timber.tag(TAG).e("mapErrorForCustomer: $error") + TangemLogger.withTag(TAG).e("mapErrorForCustomer: $error") error.mapErrorForCustomer() } .map { customerInfo -> - Timber.tag(TAG).i("customerInfo") + TangemLogger.withTag(TAG).i("customerInfo") if (customerInfo.productInstance == null) { onboardingRepository.createOrder(userWalletId) - Timber.tag("ddk9499").d("TangemPayMainScreenCustomerInfoUseCase.proceedWithoutOrder: ") MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.NEW) } else { MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.COMPLETED) diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 47d5fd6e1c..4d61929513 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -52,7 +52,6 @@ dependencies { /** Other libraries */ implementation(platform(deps.firebase.bom)) implementation(deps.firebase.analytics) - implementation(deps.timber) // region DI implementation(deps.hilt.android) diff --git a/domain/wallets/models/build.gradle.kts b/domain/wallets/models/build.gradle.kts index 1af28af669..83c2d1f222 100644 --- a/domain/wallets/models/build.gradle.kts +++ b/domain/wallets/models/build.gradle.kts @@ -12,13 +12,14 @@ dependencies { // endregion // region Domain modules - implementation(project(":domain:models")) + implementation(projects.domain.models) // endregion + implementation(projects.core.utils) + // region Other libraries implementation(deps.kotlin.serialization) implementation(deps.moshi.kotlin) - implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) // endregion } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SyncWalletWithRemoteUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SyncWalletWithRemoteUseCase.kt index 1f111425fc..063dd3380d 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SyncWalletWithRemoteUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SyncWalletWithRemoteUseCase.kt @@ -3,7 +3,7 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.runSuspendCatching -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Use case to sync wallet with remote @@ -16,6 +16,6 @@ class SyncWalletWithRemoteUseCase( suspend operator fun invoke(userWalletId: UserWalletId) { runSuspendCatching { walletsRepository.createWallet(userWalletId) } - .onFailure(Timber::e) + .onFailure { TangemLogger.e("Error", it) } } } \ No newline at end of file diff --git a/domain/yield-supply/build.gradle.kts b/domain/yield-supply/build.gradle.kts index e8a423d566..ea83aa3225 100644 --- a/domain/yield-supply/build.gradle.kts +++ b/domain/yield-supply/build.gradle.kts @@ -15,6 +15,7 @@ tasks.withType().configureEach { dependencies { /** Core */ implementation(projects.core.ui) + implementation(projects.core.utils) /** Domain */ implementation(projects.domain.account.status) @@ -34,7 +35,6 @@ dependencies { /** Other */ implementation(deps.arrow.core) - implementation(deps.timber) /** tests */ testImplementation(projects.common.test) diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt index 9a9da874ed..71f832affc 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEstimateEnterFeeUseCase.kt @@ -14,7 +14,7 @@ import com.tangem.domain.yield.supply.INCREASE_GAS_LIMIT_FOR_SUPPLY import com.tangem.domain.yield.supply.fixFee import com.tangem.domain.yield.supply.increaseGasLimitBy import com.tangem.utils.extensions.isSingleItem -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger class YieldSupplyEstimateEnterFeeUseCase( private val feeRepository: FeeRepository, @@ -65,7 +65,7 @@ class YieldSupplyEstimateEnterFeeUseCase( val estimatedFees = blockAidGasEstimate.getGasEstimation( cryptoCurrency = cryptoCurrency, transactionDataList = transactionDataList, - ).onLeft(Timber::e).getOrNull() ?: return null + ).onLeft { TangemLogger.e("Error", it) }.getOrNull() ?: return null if (estimatedFees.estimatedGasList.isEmpty()) return null diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTracker.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTracker.kt index 8693bae476..9b202cc6ed 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTracker.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTracker.kt @@ -4,14 +4,15 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.yield.supply.YieldSupplyRepository +import com.tangem.domain.yield.supply.usecase.YieldSupplyPendingTracker.Companion.CHECK_INTERVAL_MS import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import timber.log.Timber import java.util.concurrent.ConcurrentHashMap /** @@ -73,7 +74,7 @@ class YieldSupplyPendingTracker( try { checkAllTracked() } catch (ex: Exception) { - Timber.e(ex) + TangemLogger.e("Error", ex) } } } diff --git a/features/account/impl/build.gradle.kts b/features/account/impl/build.gradle.kts index b6439a01c3..1c4ab8d7a1 100644 --- a/features/account/impl/build.gradle.kts +++ b/features/account/impl/build.gradle.kts @@ -66,7 +66,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) - implementation(deps.timber) implementation(deps.firebase.crashlytics) /** DI */ diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt index cde8aabbcb..57ca6f87e6 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt @@ -25,10 +25,10 @@ import com.tangem.features.account.createedit.error.AccountFeatureError import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -147,7 +147,7 @@ internal class ArchivedAccountListModel @Inject constructor( private fun logError(error: AccountFeatureError, params: Map = emptyMap()) { val exception = IllegalStateException(error.toString()) - Timber.e(exception) + TangemLogger.e("Error", exception) analyticsExceptionHandler.sendException( event = ExceptionAnalyticsEvent(exception = exception, params = params), diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUMBuilder.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUMBuilder.kt index 737b5f6e86..2c64642433 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUMBuilder.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/entity/AccountArchivedUMBuilder.kt @@ -8,8 +8,8 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.usecase.ArchivedAccountList import com.tangem.domain.models.account.AccountId +import com.tangem.utils.logging.TangemLogger import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber import javax.inject.Inject internal class AccountArchivedUMBuilder @Inject constructor() { @@ -49,7 +49,7 @@ internal class AccountArchivedUMBuilder @Inject constructor() { onCloseClick: () -> Unit, getArchivedAccounts: () -> Unit, ): AccountArchivedUM.Error { - Timber.e(throwable) + TangemLogger.e("Error", throwable) return AccountArchivedUM.Error( onCloseClick = onCloseClick, onRetryClick = { getArchivedAccounts() }, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index 5ef205ef80..50a4eaf8b6 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -42,11 +42,11 @@ import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder. import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.updateName import com.tangem.features.account.createedit.error.AccountFeatureError import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @ModelScoped @@ -242,7 +242,7 @@ internal class AccountCreateEditModel @Inject constructor( private fun onNameChange(name: AccountNameUM) { val isNotEmptyCustomName = (name as? AccountNameUM.Custom)?.raw?.isNotEmpty() == true if (!name.isValidName() && isNotEmptyCustomName) { - Timber.d("Invalid account name: $name") + TangemLogger.d("Invalid account name: $name") return } @@ -310,7 +310,7 @@ internal class AccountCreateEditModel @Inject constructor( private fun logError(error: AccountFeatureError, params: Map = emptyMap()) { val exception = IllegalStateException(error.toString()) - Timber.e(exception) + TangemLogger.e("Error", exception) analyticsExceptionHandler.sendException( event = ExceptionAnalyticsEvent(exception = exception, params = params), diff --git a/features/approval/impl/build.gradle.kts b/features/approval/impl/build.gradle.kts index e15b2662cd..2e43f87aee 100644 --- a/features/approval/impl/build.gradle.kts +++ b/features/approval/impl/build.gradle.kts @@ -50,7 +50,6 @@ dependencies { /** Other */ implementation(deps.decompose) implementation(deps.decompose.ext.compose) - implementation(deps.timber) implementation(deps.kotlin.immutable.collections) implementation(deps.arrow.core) diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt index 87882d24ce..3d328cc6bf 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt @@ -8,6 +8,7 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic @@ -15,6 +16,7 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.models.currency.CryptoCurrency @@ -27,19 +29,17 @@ import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.approval.api.GiveApprovalComponent import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback import com.tangem.features.send.v2.api.entity.FeeSelectorUM -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.common.ui.userwallet.ext.walletInterationIcon -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject @@ -183,7 +183,7 @@ internal class GiveApprovalModel @Inject constructor( contractAddress = tokenCurrency.contractAddress, spenderAddress = params.spenderAddress, ).getOrElse { error -> - Timber.e(error, "Failed to create approval transaction") + TangemLogger.e("Failed to create approval transaction", error) return false } @@ -201,7 +201,7 @@ internal class GiveApprovalModel @Inject constructor( ) }.fold( ifLeft = { error -> - Timber.e("Failed to send approval transaction: $error") + TangemLogger.e("Failed to send approval transaction: $error") false }, ifRight = { diff --git a/features/biometry/impl/build.gradle.kts b/features/biometry/impl/build.gradle.kts index def97bf1f3..075c304c23 100644 --- a/features/biometry/impl/build.gradle.kts +++ b/features/biometry/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.core.analytics.models) implementation(projects.core.configToggles) implementation(projects.core.navigation) + implementation(projects.core.utils) /** Domain */ implementation(projects.domain.wallets) @@ -45,7 +46,6 @@ dependencies { } /** Other */ - implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt index 78cd72167e..116d0fd053 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/model/AskBiometryModel.kt @@ -22,13 +22,13 @@ import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.biometry.impl.ui.state.AskBiometryUM import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -88,7 +88,7 @@ internal class AskBiometryModel @Inject constructor( * because it will be automatically saved on UserWalletsListManager switch */ val selectedUserWallet = getSelectedWalletUseCase.sync().getOrNull() ?: run { - Timber.e("Unable to save user wallet") + TangemLogger.e("Unable to save user wallet") uiMessageSender.send( SnackbarMessage(stringReference("No selected user wallet")), ) diff --git a/features/create-wallet-selection/impl/build.gradle.kts b/features/create-wallet-selection/impl/build.gradle.kts index 458b4b5048..8fd9175d79 100644 --- a/features/create-wallet-selection/impl/build.gradle.kts +++ b/features/create-wallet-selection/impl/build.gradle.kts @@ -68,7 +68,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) - implementation(deps.timber) implementation(deps.firebase.crashlytics) /** DI */ diff --git a/features/create-wallet-start/impl/build.gradle.kts b/features/create-wallet-start/impl/build.gradle.kts index 1578c1916f..c280d4160c 100644 --- a/features/create-wallet-start/impl/build.gradle.kts +++ b/features/create-wallet-start/impl/build.gradle.kts @@ -66,7 +66,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) - implementation(deps.timber) implementation(deps.firebase.crashlytics) /** DI */ diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt index ac00b3703e..4a864f1ca9 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt @@ -40,7 +40,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val HIDE_PROGRESS_DELAY = 400L @@ -211,7 +211,7 @@ internal class CreateWalletStartModel @Inject constructor( val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() if (userWallet == null) { - Timber.e("User wallet not created") + TangemLogger.e("User wallet not created") setLoading(false) return } @@ -221,7 +221,7 @@ internal class CreateWalletStartModel @Inject constructor( delay(HIDE_PROGRESS_DELAY) setLoading(false) when (error) { - is SaveWalletError.DataError -> Timber.e(error.toString(), "Unable to save user wallet") + is SaveWalletError.DataError -> TangemLogger.e("Unable to save user wallet: $error") is SaveWalletError.WalletAlreadySaved -> { userWalletsListRepository.unlock( userWalletId = userWallet.walletId, @@ -246,8 +246,8 @@ internal class CreateWalletStartModel @Inject constructor( private fun handleScanError(error: TangemError) { when (error) { is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() - is TangemSdkError -> Timber.e(error, "Scan error occurred") - else -> Timber.e(error, "Error happened") + is TangemSdkError -> TangemLogger.e("Scan error occurred", error) + else -> TangemLogger.e("Error happened", error) } } diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index 19d925cad1..742988403b 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.core.configToggles) implementation(projects.core.navigation) implementation(projects.core.analytics) + implementation(projects.core.utils) implementation(projects.core.analytics.models) implementation(projects.common.routing) implementation(projects.common.ui) @@ -78,7 +79,6 @@ dependencies { /* Other */ implementation(deps.kotlin.immutable.collections) implementation(deps.reKotlin) - implementation(deps.timber) implementation(deps.arrow.core) implementation(deps.arrow.fx) } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index fb9104d96e..a4d4c58b0a 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.details.model import android.content.res.Resources import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -45,7 +46,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import timber.log.Timber import java.util.Locale import javax.inject.Inject @@ -84,8 +84,8 @@ internal class DetailsModel @Inject constructor( val isWalletConnectAvailable = runBlocking { // danger region, this works immediately, but will be refactored later with WC - checkIsWalletConnectAvailableUseCase(params.userWalletId).getOrElse { - Timber.w("Unable to check WalletConnect availability: $it") + checkIsWalletConnectAvailableUseCase(params.userWalletId).getOrElse { throwable -> + TangemLogger.w("Unable to check WalletConnect availability: $throwable") false } diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt index 44af704d49..5c00ab323d 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -29,7 +29,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -115,7 +115,7 @@ internal class UserWalletListModel @Inject constructor( unlockWalletUseCase(userWalletId) .onRight { router.push(AppRoute.WalletSettings(userWalletId)) } .onLeft { error -> - Timber.e("Failed to unlock wallet $userWalletId: $error") + TangemLogger.e("Failed to unlock wallet $userWalletId: $error") error.handle( onUserCancelled = {}, isFromUnlockAll = false, @@ -142,7 +142,7 @@ internal class UserWalletListModel @Inject constructor( applyUserWalletListSortingUseCase(userWalletIds).onRight { analyticsEventHandler.send(WalletSettingsAnalyticEvents.WalletsReorder()) }.onLeft { error -> - Timber.e("Failed to apply wallet list sorting: $error") + TangemLogger.e("Failed to apply wallet list sorting: $error") } } } diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts index 7c72408c05..58d95cba7f 100644 --- a/features/disclaimer/impl/build.gradle.kts +++ b/features/disclaimer/impl/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { /** Other dependencies */ implementation(deps.arrow.core) - implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt index 5265f434bf..110e0ae876 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt @@ -5,7 +5,7 @@ import android.webkit.WebResourceError import android.webkit.WebResourceRequest import android.webkit.WebView import com.google.accompanist.web.AccompanistWebViewClient -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * [AccompanistWebViewClient] for Disclaimer [WebView] @@ -24,7 +24,7 @@ internal class DisclaimerWebViewClient( override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) - Timber.d("onPageStarted: $url") + TangemLogger.d("onPageStarted: $url") if (url != null) { loadedUrls[url] = false } @@ -33,7 +33,7 @@ internal class DisclaimerWebViewClient( override fun onPageFinished(view: WebView?, url: String?) { super.onPageFinished(view, url) - Timber.d("onPageFinished: $url") + TangemLogger.d("onPageFinished: $url") if (url != null && loadedUrls.containsKey(url)) { loadedUrls[url] = true onLoadingFinished(false) @@ -45,7 +45,7 @@ internal class DisclaimerWebViewClient( val url = request?.url?.toString() - Timber.d("onReceivedError: $url") + TangemLogger.d("onReceivedError: $url") if (url != null && loadedUrls.containsKey(url)) { loadedUrls[url] = true diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts index 95ac903999..2e076661b9 100644 --- a/features/feed/impl/build.gradle.kts +++ b/features/feed/impl/build.gradle.kts @@ -75,7 +75,6 @@ dependencies { /* Other */ implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(deps.decompose.ext.compose) /* Core */ diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt index 0d733ab09f..ce12bfc05a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -34,7 +34,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val TOKEN_ACTIONS_DELAY = 500L @@ -205,7 +205,7 @@ internal class AddToPortfolioModel @Inject constructor( finishFlow() } .catch { throwable -> - Timber.e(throwable) + TangemLogger.e("Error", throwable) params.callback.onDismiss() } .launchIn(modelScope) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt index 79ca66cf2b..60acc73b60 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt @@ -29,7 +29,7 @@ import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import javax.inject.Inject @@ -134,7 +134,7 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor( finishSuccessFlow(addedToken.currency, selectedPortfolioValue.userWallet.walletId) } .catch { throwable -> - Timber.e(throwable) + TangemLogger.e("Error", throwable) params.callback.onDismiss() } .launchIn(modelScope) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt index feb3025f69..8b32b2497b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt @@ -11,7 +11,7 @@ import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.feed.impl.R -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject class CheckCurrencyUnsupportedDelegate @Inject constructor( @@ -29,14 +29,14 @@ class CheckCurrencyUnsupportedDelegate @Inject constructor( networkId = rawNetworkId, isMainNetwork = isMainNetwork, ).getOrElse { throwable -> - Timber.e( - throwable, + TangemLogger.e( """ Failed to check currency unsupported state |- User wallet ID: $userWalletId |- Network ID: $rawNetworkId |- Is main network: $isMainNetwork """.trimIndent(), + throwable, ) val message = SnackbarMessage( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt index 3ef3be6ce2..89c501508c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultMarketsTokenDetailDeepLinkHandler.kt @@ -16,7 +16,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject constructor( @Assisted private val scope: CoroutineScope, @@ -44,7 +44,7 @@ internal class DefaultMarketsTokenDetailDeepLinkHandler @AssistedInject construc tokenId = rawTokenId, tokenSymbol = "", // used for analytics ).getOrElse { - Timber.e("Failed to get market token info") + TangemLogger.e("Failed to get market token info") return@launch } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDetailsDeepLinkHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDetailsDeepLinkHandler.kt index 8409d68360..0419dfe414 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDetailsDeepLinkHandler.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/deeplink/DefaultNewsDetailsDeepLinkHandler.kt @@ -9,7 +9,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultNewsDetailsDeepLinkHandler @AssistedInject constructor( @Assisted private val scope: CoroutineScope, @@ -25,7 +25,7 @@ internal class DefaultNewsDetailsDeepLinkHandler @AssistedInject constructor( scope.launch { val articleId = extractArticleIdFromUri(deeplinkUri) if (articleId == null) { - Timber.e( + TangemLogger.e( """ Failed to extract article ID from deep link |- Received URI: $deeplinkUri diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/utils/LoggingUtils.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/utils/LoggingUtils.kt index 03579058d8..d3ba6deb04 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/utils/LoggingUtils.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/utils/LoggingUtils.kt @@ -6,10 +6,10 @@ import com.tangem.domain.markets.TokenMarketUpdateRequest import com.tangem.pagination.BatchAction import com.tangem.pagination.BatchUpdateResult import com.tangem.pagination.PaginationStatus -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal fun logStatus(tag: String, status: PaginationStatus>) { - Timber.tag(tag).d( + TangemLogger.withTag(tag).d( """ Status $status @@ -19,19 +19,19 @@ internal fun logStatus(tag: String, status: PaginationStatus>) internal fun logAction(tag: String, action: BatchAction) { when (action) { - is BatchAction.Reload -> Timber.tag(tag).d( + is BatchAction.Reload -> TangemLogger.withTag(tag).d( """ Reload = ${action.requestParams} """.trimIndent(), ) - is BatchAction.UpdateBatches -> Timber.tag(tag).d( + is BatchAction.UpdateBatches -> TangemLogger.withTag(tag).d( """ To update: keys: ${action.keys.toList()} updateType: ${action.updateRequest.javaClass.simpleName} """.trimIndent(), ) - else -> Timber.tag(tag).d( + else -> TangemLogger.withTag(tag).d( """ $action """.trimIndent(), @@ -48,7 +48,7 @@ internal fun logUpdateResults( is BatchUpdateResult.Error -> s.throwable.toString() } - Timber.tag(tag).d( + TangemLogger.withTag(tag).d( """ updateResults request: ${updateResult.first} diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt index 63cc26391a..bdd662e7f2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt @@ -35,7 +35,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.util.Locale import javax.inject.Inject @@ -134,7 +134,7 @@ internal class NewsDetailsModel @Inject constructor( modelScope.launch { toggleArticleLikedUseCase .toggleLiked(articleId) - .onLeft { Timber.e(it) } + .onLeft { TangemLogger.e("Error", it) } analyticsEventHandler.send(NewsDetailsAnalyticsEvent.NewsLikeClicked(articleId)) } } @@ -231,7 +231,7 @@ internal class NewsDetailsModel @Inject constructor( } // global request executing error newsId < 0 -> { - Timber.e(error) + TangemLogger.e("Error", error) } else -> { val (code, message) = when (error) { diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts index bd620457d9..afa7941725 100644 --- a/features/home/impl/build.gradle.kts +++ b/features/home/impl/build.gradle.kts @@ -22,7 +22,8 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.navigation) - + implementation(projects.core.utils) + /** Common */ implementation(projects.common.routing) @@ -63,7 +64,6 @@ dependencies { /** Other libraries */ implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt index 7cb91f3880..63a48788e4 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -48,7 +48,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.util.Locale import javax.inject.Inject @@ -196,7 +196,7 @@ internal class HomeModel @Inject constructor( val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() if (userWallet == null) { - Timber.e("User wallet not created") + TangemLogger.e("User wallet not created") setLoading(false) return } @@ -205,11 +205,11 @@ internal class HomeModel @Inject constructor( userWallet = userWallet, analyticsSource = AnalyticsParam.ScreensSources.Intro, ).fold( - ifLeft = { + ifLeft = { error -> delay(HIDE_PROGRESS_DELAY) setLoading(false) - when (it) { - is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + when (error) { + is SaveWalletError.DataError -> TangemLogger.e("Unable to save user wallet: $error") is SaveWalletError.WalletAlreadySaved -> appRouter.replaceAll(AppRoute.Wallet) } }, @@ -245,8 +245,8 @@ internal class HomeModel @Inject constructor( private fun handleScanError(error: TangemError) { when (error) { is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() - is TangemSdkError -> Timber.e(error, "Scan error occurred") - else -> Timber.e(error, "Error happened") + is TangemSdkError -> TangemLogger.e("Scan error occurred", error) + else -> TangemLogger.e("Error happened", error) } } diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index 380b51678e..37c0ca6773 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -72,7 +72,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) - implementation(deps.timber) implementation(deps.firebase.crashlytics) /** DI */ diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index 1a2cb122b6..f23c52ad2f 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -26,7 +26,7 @@ import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @ModelScoped @@ -53,7 +53,7 @@ internal class HotAccessCodeRequestModel @Inject constructor( suspend fun show(attemptRequest: HotWalletPasswordRequester.AttemptRequest) { if (userWalletExists(attemptRequest.hotWalletId).not()) { - Timber.e("User wallet with id ${attemptRequest.hotWalletId} does not exist") + TangemLogger.e("User wallet with id ${attemptRequest.hotWalletId} does not exist") result.value = HotWalletPasswordRequester.Result.Dismiss return } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index e2d4d75a14..eb4f297ea2 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.hotwallet.addexistingwallet.im.port.model +import com.tangem.utils.logging.TangemLogger import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent @@ -28,7 +29,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -96,10 +96,10 @@ internal class AddExistingWalletImportModel @Inject constructor( val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId) val userWallet = hotUserWalletBuilder.build() saveUserWalletUseCase.invoke(userWallet.copy(backedUp = true)) - .onLeft { + .onLeft { error -> setImportProgress(false) - when (it) { - is SaveWalletError.DataError -> Timber.e(it.toString(), "Unable to save user wallet") + when (error) { + is SaveWalletError.DataError -> TangemLogger.e("Unable to save user wallet: $error") is SaveWalletError.WalletAlreadySaved -> { uiMessageSender.send( SnackbarMessage(resourceReference(R.string.hw_import_seed_phrase_already_imported)), @@ -129,8 +129,8 @@ internal class AddExistingWalletImportModel @Inject constructor( ) params.callbacks.onWalletImported(userWallet.walletId) } - }.onFailure { - Timber.e(it) + }.onFailure { throwable -> + TangemLogger.e("Error", throwable) setImportProgress(false) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt index 49c8bc6351..d3e5315e41 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt @@ -34,7 +34,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val HIDE_PROGRESS_DELAY = 400L @@ -129,7 +129,7 @@ internal class CreateHardwareWalletModel @Inject constructor( val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build() if (userWallet == null) { - Timber.e("User wallet not created") + TangemLogger.e("User wallet not created") setLoading(false) return } @@ -142,7 +142,7 @@ internal class CreateHardwareWalletModel @Inject constructor( delay(HIDE_PROGRESS_DELAY) setLoading(false) when (saveWalletError) { - is SaveWalletError.DataError -> Timber.e(saveWalletError.toString(), "Unable to save user wallet") + is SaveWalletError.DataError -> TangemLogger.e("Unable to save user wallet: $saveWalletError") is SaveWalletError.WalletAlreadySaved -> handleAlreadySavedCard( saveWalletError.messageId, walletId = userWallet.walletId, @@ -164,8 +164,8 @@ internal class CreateHardwareWalletModel @Inject constructor( private fun handleScanError(error: TangemError) { when (error) { is TangemSdkError.NfcFeatureIsUnavailable -> handleNfcFeatureUnavailable() - is TangemSdkError -> Timber.e(error, "Scan error occurred") - else -> Timber.e(error, "Error happened") + is TangemSdkError -> TangemLogger.e("Scan error occurred", error) + else -> TangemLogger.e("Error happened", error) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index dad097a1fb..a5c4e17316 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -28,7 +28,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -112,7 +112,7 @@ internal class CreateMobileWalletModel @Inject constructor( router.replaceAll(AppRoute.Wallet) }.onFailure { throwable -> - Timber.e(throwable) + TangemLogger.e("Error", throwable) uiState.update { it.copy(createButtonLoading = false) } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt index a4d9c89ffb..872ba5c072 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.hotwallet.forgetwallet import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -20,7 +21,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @ModelScoped @@ -80,8 +80,8 @@ internal class ForgetWalletModel @Inject constructor( private fun forgetWallet() { modelScope.launch { val hasUserWallets = deleteWalletUseCase(params.userWalletId) - .getOrElse { - Timber.e("Unable to delete wallet: $it") + .getOrElse { error -> + TangemLogger.e("Unable to delete wallet: $error") uiMessageSender.send( message = SnackbarMessage(resourceReference(R.string.common_unknown_error)), diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt index b68069c4fe..3006348ca2 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/check/model/ManualBackupCheckModel.kt @@ -3,6 +3,7 @@ package com.tangem.features.hotwallet.manualbackup.check.model import androidx.compose.runtime.Stable import androidx.compose.ui.text.input.TextFieldValue import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -20,15 +21,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject -import kotlin.Boolean -import kotlin.Int -import kotlin.String -import kotlin.Suppress -import kotlin.collections.List -import kotlin.collections.all -import kotlin.collections.map @Stable @ModelScoped @@ -63,7 +56,7 @@ internal class ManualBackupCheckModel @Inject constructor( } } }.onFailure { - Timber.e(it) + TangemLogger.e("Error", it) } } } @@ -114,8 +107,8 @@ internal class ManualBackupCheckModel @Inject constructor( uiState.update { it.copy(completeButtonProgress = false) } - }.onFailure { - Timber.e(it) + }.onFailure { throwable -> + TangemLogger.e("Error", throwable) uiState.update { it.copy(completeButtonProgress = false) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt index abb3ea0654..f4c716ed2b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/manualbackup/phrase/model/ManualBackupPhraseModel.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Stable @@ -58,7 +58,7 @@ internal class ManualBackupPhraseModel @Inject constructor( } } }.onFailure { - Timber.e(it) + TangemLogger.e("Error", it) } } } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/model/ViewPhraseModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/model/ViewPhraseModel.kt index 808f1df0b6..eb373c4429 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/model/ViewPhraseModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/viewphrase/model/ViewPhraseModel.kt @@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Stable @@ -53,7 +53,7 @@ internal class ViewPhraseModel @Inject constructor( modelScope.launch { val words = exportSeedPhraseUseCase.invoke(userWallet.hotWalletId) - .getOrElse { error -> Timber.e(error); throw error } + .getOrElse { error -> TangemLogger.e("Error", error); throw error } .mnemonic.mnemonicComponents uiState.update { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt index 7c284f9cef..32986a6549 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/walletbackup/model/WalletBackupModel.kt @@ -21,7 +21,7 @@ import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject @@ -71,7 +71,7 @@ internal class WalletBackupModel @Inject constructor( .onEach { either -> either.fold( ifLeft = { - Timber.e("Error on getting user wallet: $it") + TangemLogger.e("Error on getting user wallet: $it") }, ifRight = { userWallet -> if (!isScreenOpenedEventSent.get() && userWallet is UserWallet.Hot) { @@ -128,13 +128,13 @@ internal class WalletBackupModel @Inject constructor( getUserWalletUseCase.invoke(params.userWalletId) .fold( ifLeft = { - Timber.e("Error on getting user wallet: $it") + TangemLogger.e("Error on getting user wallet: $it") }, ifRight = { userWallet -> when (userWallet) { is UserWallet.Cold -> { val userWalletId = userWallet.walletId - Timber.e("Unexpected cold wallet when request seed phrase: $userWalletId") + TangemLogger.e("Unexpected cold wallet when request seed phrase: $userWalletId") } is UserWallet.Hot -> showSeedPhrase(userWallet) } @@ -155,7 +155,7 @@ internal class WalletBackupModel @Inject constructor( unlockHotWalletContextualUseCase.invoke(hotWallet.hotWalletId) .fold( ifLeft = { - Timber.e("Error while export seed phrase: $it") + TangemLogger.e("Error while export seed phrase: $it") }, ifRight = { seedPhrasePrivateInfo -> router.push(AppRoute.ViewPhrase(params.userWalletId)) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt index c432ac4cc1..d7a12e7a10 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/wallethardwarebackup/model/WalletHardwareBackupModel.kt @@ -35,7 +35,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -147,7 +147,7 @@ internal class WalletHardwareBackupModel @Inject constructor( -> modelScope.launch { unlockHotWalletContextualUseCase.invoke(hotWalletId) .onLeft { - Timber.e(it, "Unable to unlock wallet with id ${params.userWalletId}") + TangemLogger.e("Unable to unlock wallet with id ${params.userWalletId}", it) } .onRight { router.push(AppRoute.UpgradeWallet(userWalletId = params.userWalletId)) diff --git a/features/kyc/impl/build.gradle.kts b/features/kyc/impl/build.gradle.kts index 1bf8fc66f2..f51dea8c7d 100644 --- a/features/kyc/impl/build.gradle.kts +++ b/features/kyc/impl/build.gradle.kts @@ -49,7 +49,6 @@ dependencies { /** Other libraries */ implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) - implementation(deps.timber) implementation(deps.firebase.crashlytics) implementation(deps.sumsub.sdk) implementation(deps.arrow.core) diff --git a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt index f729fe489c..d181f971dc 100644 --- a/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt +++ b/features/kyc/impl/src/main/kotlin/com/tangem/features/kyc/DefaultKycModel.kt @@ -13,7 +13,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Stable @@ -40,7 +40,7 @@ class DefaultKycModel @Inject constructor( tangemPayEligibilityManager.reset() } } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) } } } diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 4a0329dcad..867d3f035c 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.configToggles) implementation(projects.core.analytics) + implementation(projects.core.utils) implementation(projects.common.routing) implementation(projects.common.ui) @@ -67,6 +68,5 @@ dependencies { /* Other */ implementation(deps.kotlin.immutable.collections) implementation(deps.decompose.ext.compose) - implementation(deps.timber) implementation(deps.reKotlin) // need for legacy onboarding } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt index 43fbc9472f..1d6522b7b1 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt @@ -41,7 +41,7 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -243,7 +243,7 @@ internal class ChooseManagedTokensModel @Inject constructor( } is PaginationStatus.Paginating -> { (status.lastResult as? BatchFetchResult.Error)?.let { fetchError -> - Timber.e(fetchError.throwable) + TangemLogger.e("Error", fetchError.throwable) } state.copy( 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 5418e2b7b1..e62d320665 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 @@ -1,6 +1,7 @@ package com.tangem.features.managetokens.model import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped @@ -36,7 +37,6 @@ import kotlinx.collections.immutable.mutate import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject // TODO: Divide to sub-components: [REDACTED_JIRA] @@ -239,7 +239,7 @@ internal class CustomTokenFormModel @Inject constructor( } private fun showErrorDialog(throwable: Throwable) { - Timber.e(throwable) + TangemLogger.e("Error", throwable) val message = when (throwable) { is TangemSdkError -> resourceReference( R.string.generic_error_code, @@ -279,7 +279,7 @@ internal class CustomTokenFormModel @Inject constructor( stateAcc // No need to display error } is CustomTokenFormValidationException.DataError -> { - Timber.e(exception.cause, "Unable to validate custom currency") + TangemLogger.e("Unable to validate custom currency", exception.cause) stateAcc } } 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 960347fc65..2c10b94d72 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 @@ -38,7 +38,7 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -248,7 +248,7 @@ internal class ManageTokensModel @Inject constructor( } is PaginationStatus.Paginating -> { (status.lastResult as? BatchFetchResult.Error)?.let { fetchError -> - Timber.e(fetchError.throwable) + TangemLogger.e("Error", fetchError.throwable) } state.copySealed( @@ -339,7 +339,7 @@ internal class ManageTokensModel @Inject constructor( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, currenciesToRemove = manageTokensListManager.currenciesToRemove.value, ).getOrElse { throwable -> - Timber.e(throwable, "Failed to save changes") + TangemLogger.e("Failed to save changes", throwable) return@resource } 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 02a234043f..4010fb57e9 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 @@ -33,7 +33,7 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -167,7 +167,7 @@ internal class OnboardingManageTokensModel @Inject constructor( } is PaginationStatus.Paginating -> { (status.lastResult as? BatchFetchResult.Error)?.let { fetchError -> - Timber.e(fetchError.throwable) + TangemLogger.e("Error", fetchError.throwable) } state.copy( @@ -261,7 +261,7 @@ internal class OnboardingManageTokensModel @Inject constructor( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, currenciesToRemove = manageTokensListManager.currenciesToRemove.value, ).getOrElse { throwable -> - Timber.e(throwable, "Failed to save changes") + TangemLogger.e("Failed to save changes", throwable) return@resource } @@ -286,7 +286,7 @@ internal class OnboardingManageTokensModel @Inject constructor( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, currenciesToRemove = manageTokensListManager.currenciesToRemove.value, ).getOrElse { throwable -> - Timber.e(throwable, "Failed to save changes") + TangemLogger.e("Failed to save changes", throwable) return@resource } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt index 698d65bd5b..d62b31c3e5 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/CustomCurrencyValidator.kt @@ -16,7 +16,7 @@ import com.tangem.utils.coroutines.saveInAndJoin import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class CustomCurrencyValidator( private val userWalletId: UserWalletId, @@ -103,7 +103,7 @@ internal class CustomCurrencyValidator( ).getOrElse { e -> when (e) { is FindTokenException.DataError -> { - Timber.e(e.cause, "Unable to find custom currency") + TangemLogger.e("Unable to find custom currency", e.cause) updateStatus(Status.UnexpectedException(e.cause)) return } @@ -135,7 +135,7 @@ internal class CustomCurrencyValidator( ).getOrElse { e -> val newStatus = when (e) { is FindTokenException.DataError -> { - Timber.e(e.cause, "Unable to find custom currency") + TangemLogger.e("Unable to find custom currency", e.cause) Status.UnexpectedException(e.cause) } is FindTokenException.NotFound -> { @@ -161,7 +161,7 @@ internal class CustomCurrencyValidator( derivationPath = derivationPath, formValues = validatedForm, ).getOrElse { e -> - Timber.e(e, "Unable to create custom currency") + TangemLogger.e("Unable to create custom currency", e) updateStatus(Status.UnexpectedException(e)) return } @@ -181,7 +181,7 @@ internal class CustomCurrencyValidator( is CryptoCurrency.Token -> currency.contractAddress }, ).getOrElse { e -> - Timber.e(e, "Unable to check if currency is already added") + TangemLogger.e("Unable to check if currency is already added", e) updateStatus(Status.UnexpectedException(e)) return diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt index ea253fd2d2..96cc6f1e8a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt @@ -5,6 +5,7 @@ import arrow.core.raise.Raise import arrow.core.raise.either import arrow.core.raise.ensureNotNull import arrow.core.right +import com.tangem.utils.logging.TangemLogger import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.domain.account.producer.SingleAccountListProducer @@ -21,7 +22,6 @@ import com.tangem.lib.crypto.derivation.AccountNodeRecognizer import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( @Assisted private val userWalletId: UserWalletId, @@ -84,14 +84,14 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( val blockchain = Blockchain.fromNetworkId(networkId = currency.network.backendId) if (blockchain == null) { val exception = IllegalStateException("Token has unknown networkId: ${currency.id}") - Timber.e(exception) + TangemLogger.e("Error", exception) raise(exception) } val derivationPathValue = currency.network.derivationPath.value if (derivationPathValue == null) { val exception = IllegalStateException("Token has no derivation path: ${currency.id}") - Timber.e(exception) + TangemLogger.e("Error", exception) raise(exception) } @@ -99,10 +99,9 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( val index = accountNodeRecognizer.recognize(derivationPathValue)?.toInt() if (index == null) { - Timber.e( - "%s%s", - "Unable to determine account index for derivation path: $derivationPathValue. ", - "Use main account index instead.", + TangemLogger.e( + "Unable to determine account index for derivation path: $derivationPathValue. " + + "Use main account index instead.", ) DerivationIndex.Main.value } else { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index 6265318696..7020d8874f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Suppress("LongParameterList", "LargeClass") internal class ManageTokensListManager @AssistedInject constructor( @@ -222,7 +222,7 @@ internal class ManageTokensListManager @AssistedInject constructor( scope.launch { useCasesFacade.removeCustomCurrencyUseCase(currency) .onRight { reload() } - .onLeft { Timber.e(it) } + .onLeft { TangemLogger.e("Error", it) } } } @@ -266,13 +266,13 @@ internal class ManageTokensListManager @AssistedInject constructor( tempAddedTokens = changedCurrenciesManager.currenciesToAdd.value, tempRemovedTokens = changedCurrenciesManager.currenciesToRemove.value, ).getOrElse { throwable -> - Timber.e( - throwable, + TangemLogger.e( """ Failed to check linked tokens |- Mode: $mode |- Network ID: ${network.id} """.trimIndent(), + throwable, ) val message = SnackbarMessage( @@ -292,13 +292,13 @@ internal class ManageTokensListManager @AssistedInject constructor( return useCasesFacade.checkCurrencyUnsupportedUseCase( sourceNetwork = sourceNetwork, ).getOrElse { throwable -> - Timber.e( - throwable, + TangemLogger.e( """ Failed to check currency unsupported state |- Mode: $mode |- Source Network: $sourceNetwork """.trimIndent(), + throwable, ) val message = SnackbarMessage( diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 86304b9fd5..bc76460d54 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -68,7 +68,6 @@ dependencies { /* Other */ implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(deps.decompose.ext.compose) /* Core */ @@ -78,6 +77,7 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.navigation) + implementation(projects.core.utils) /* Common */ implementation(projects.common.ui) diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index 693be634b3..7db2bdc36d 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -73,7 +73,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) - implementation(deps.timber) implementation(deps.firebase.crashlytics) /** DI */ diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt index 2f6f77b6c7..13fc9173de 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt @@ -21,7 +21,7 @@ import com.tangem.features.nft.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -136,7 +136,7 @@ internal class NFTCollectionsModel @Inject constructor( _state.update { ChangeRefreshingStateTransformer(true).transform(it) } try { refreshAllNFTUseCase(params.userWalletId) - .onLeft { Timber.e(it) } + .onLeft { TangemLogger.e("Error", it) } } finally { _state.update { ChangeRefreshingStateTransformer(false).transform(it) } } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt index e47a5fca31..4a91c998a3 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/details/model/NFTDetailsModel.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -115,7 +115,7 @@ internal class NFTDetailsModel @Inject constructor( getNFTPriceUseCase(params.userWalletId, params.nftAsset) .fold( ifLeft = { - Timber.w(it) + TangemLogger.w("Error", it) }, ifRight = { quoteFlow -> quoteFlow diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index a992e7daea..917e123120 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -81,7 +81,6 @@ dependencies { /** Other libraries */ implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) - implementation(deps.timber) implementation(deps.firebase.crashlytics) implementation(tangemDeps.card.core) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index b1893158d7..924382dc55 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.onboarding.v2.twin.impl.model +import com.tangem.utils.logging.TangemLogger import com.tangem.Message import com.tangem.common.CompletionResult import com.tangem.common.KeyPair @@ -47,7 +48,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -296,7 +296,7 @@ internal class OnboardingTwinModel @Inject constructor( private suspend fun finishActivation(scanResponse: ScanResponse) = coroutineScope { val userWallet = coldUserWalletBuilderFactory.create(scanResponse).build() ?: run { - Timber.e("User wallet not created") + TangemLogger.e("User wallet not created") setLoading(false) return@coroutineScope } @@ -305,8 +305,8 @@ internal class OnboardingTwinModel @Inject constructor( userWallet = userWallet, canOverride = true, analyticsSource = AnalyticsParam.ScreensSources.Onboarding, - ).onLeft { - Timber.e("Unable to save user wallet: $it") + ).onLeft { error -> + TangemLogger.e("Unable to save user wallet: $error") setLoading(false) return@coroutineScope } @@ -326,7 +326,7 @@ internal class OnboardingTwinModel @Inject constructor( modelScope.launch { val userWallet = coldUserWalletBuilderFactory.create(params.scanResponse).build() ?: run { - Timber.e("User wallet not created") + TangemLogger.e("User wallet not created") setLoading(false) return@launch } @@ -335,8 +335,8 @@ internal class OnboardingTwinModel @Inject constructor( userWallet = userWallet, canOverride = true, analyticsSource = AnalyticsParam.ScreensSources.Onboarding, - ).onLeft { - Timber.e("Unable to save user wallet: $it") + ).onLeft { error -> + TangemLogger.e("Unable to save user wallet: $error") setLoading(false) return@launch } diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index fcab39bc50..4c6b3a90a3 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -82,5 +82,4 @@ dependencies { 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/alloffers/model/AllOffersModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt index 5b0c5748d3..89dd1d8ecf 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt @@ -21,7 +21,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject internal class AllOffersModel @Inject constructor( @@ -127,7 +127,7 @@ internal class AllOffersModel @Inject constructor( } private fun handleOnrampError(onrampError: OnrampError) { - Timber.e(onrampError.toString()) + TangemLogger.e(onrampError.toString()) state.update { stateFactory.getOnrampErrorState(onrampError) } } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyDeepLinkHandler.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyDeepLinkHandler.kt index 5633cf6424..4210dfcdc6 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyDeepLinkHandler.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultBuyDeepLinkHandler.kt @@ -5,7 +5,7 @@ import com.tangem.common.routing.AppRouter import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultBuyDeepLinkHandler @AssistedInject constructor( router: AppRouter, @@ -16,7 +16,7 @@ internal class DefaultBuyDeepLinkHandler @AssistedInject constructor( // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet getSelectedWalletSyncUseCase().fold( ifLeft = { - Timber.e("Error on getting user wallet: $it") + TangemLogger.e("Error on getting user wallet: $it") }, ifRight = { userWallet -> router.push(AppRoute.BuyCrypto(userWallet.walletId)) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt index 035c32ef13..ff2e4d0a3e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt @@ -8,7 +8,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultOnrampDeepLinkHandler @AssistedInject constructor( @Assisted scope: CoroutineScope, @@ -36,7 +36,7 @@ internal class DefaultOnrampDeepLinkHandler @AssistedInject constructor( } } else -> { - Timber.e( + TangemLogger.e( """ Invalid parameters for ONRAMP deeplink |- Params: $queryParams diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSellDeepLinkHandler.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSellDeepLinkHandler.kt index ee38f9501a..eb25b50f21 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSellDeepLinkHandler.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSellDeepLinkHandler.kt @@ -5,7 +5,7 @@ import com.tangem.common.routing.AppRouter import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultSellDeepLinkHandler @AssistedInject constructor( router: AppRouter, @@ -16,7 +16,7 @@ internal class DefaultSellDeepLinkHandler @AssistedInject constructor( // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet getSelectedWalletSyncUseCase().fold( ifLeft = { - Timber.e("Error on getting user wallet: $it") + TangemLogger.e("Error on getting user wallet: $it") }, ifRight = { userWallet -> router.push(AppRoute.SellCrypto(userWallet.walletId)) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSwapDeepLinkHandler.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSwapDeepLinkHandler.kt index 7fff8ef48c..584aadf822 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSwapDeepLinkHandler.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultSwapDeepLinkHandler.kt @@ -5,7 +5,7 @@ import com.tangem.common.routing.AppRouter import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultSwapDeepLinkHandler @AssistedInject constructor( router: AppRouter, @@ -16,7 +16,7 @@ internal class DefaultSwapDeepLinkHandler @AssistedInject constructor( // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet getSelectedWalletSyncUseCase().fold( ifLeft = { - Timber.e("Error on getting user wallet: $it") + TangemLogger.e("Error on getting user wallet: $it") }, ifRight = { userWallet -> router.push(AppRoute.SwapCrypto(userWallet.walletId)) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt index bf19ec5d37..cf1badae14 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt @@ -41,7 +41,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -187,7 +187,7 @@ internal class HotCryptoModel @Inject constructor( channel.close() } .catch { throwable -> - Timber.e(throwable) + TangemLogger.e("Error", throwable) closeNavigationFlow() } .launchIn(modelScope) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt index 9863a66e16..cb33ca61de 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -56,13 +56,13 @@ internal class OnrampAddToPortfolioModel @Inject constructor( private fun isTangemIconVisible(): Boolean { return getUserWalletUseCase(params.userWalletId) - .onLeft { Timber.e("Unable to get wallet by id [${params.userWalletId}]: $it") } + .onLeft { TangemLogger.e("Unable to get wallet by id [${params.userWalletId}]: $it") } .fold(ifLeft = { false }, ifRight = { it is UserWallet.Cold }) } private fun getUserWalletName(): String { return getUserWalletUseCase(params.userWalletId) - .onLeft { Timber.e("Unable to get wallet name by id [${params.userWalletId}]: $it") } + .onLeft { TangemLogger.e("Unable to get wallet name by id [${params.userWalletId}]: $it") } .fold(ifLeft = { "" }, ifRight = UserWallet::name) } @@ -74,7 +74,7 @@ internal class OnrampAddToPortfolioModel @Inject constructor( manageCryptoCurrenciesUseCase(accountId = accountId, add = params.cryptoCurrency) .onRight { params.onSuccessAdding(params.cryptoCurrency.id) } .onLeft { throwable -> - Timber.e("Failed to add crypto currency: $throwable") + TangemLogger.e("Failed to add crypto currency: $throwable") changeAddButtonProgressStatus(isProgress = false) } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index b72f54919d..63d3cce362 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -29,7 +29,7 @@ import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.isNullOrZero import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -351,7 +351,7 @@ internal class OnrampMainComponentModel @Inject constructor( } private fun handleOnrampError(onrampError: OnrampError) { - Timber.e(onrampError.toString()) + TangemLogger.e(onrampError.toString()) state.update { stateFactory.getOnrampErrorState(onrampError) } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt index 95cab56383..ec8ba318be 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.util.Locale import javax.inject.Inject @@ -116,7 +116,7 @@ internal class SelectProviderModel @Inject constructor( } .onLeft { error -> sendOnrampErrorEvent(error) - Timber.e(error.toString()) + TangemLogger.e(error.toString()) } } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt index a550929c72..1540f81f69 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt @@ -32,7 +32,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -133,7 +133,7 @@ internal class OnrampRedirectModel @Inject constructor( } private fun handleError(error: OnrampError) { - Timber.e(error.toString()) + TangemLogger.e(error.toString()) analyticsEventHandler.sendOnrampErrorEvent( error = error, tokenSymbol = params.cryptoCurrency.symbol, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index 3fc7167dfc..dbae3fb93d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -40,7 +40,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import kotlin.properties.Delegates @@ -89,12 +89,12 @@ internal class OnrampSuccessComponentModel @Inject constructor( getOnrampTransactionUseCase(txId = params.txId) .fold( ifLeft = { error -> - Timber.e(error.toString()) + TangemLogger.e(error.toString()) showErrorAlert(error) }, ifRight = { transaction -> userWallet = getUserWalletUseCase(transaction.userWalletId).getOrElse { - Timber.e("UserWallet found") + TangemLogger.e("UserWallet not found") // this case should never happened showErrorAlert(OnrampError.DomainError("UserWallet not found")) return@launch @@ -104,7 +104,7 @@ internal class OnrampSuccessComponentModel @Inject constructor( ?.getCryptoCurrency(currencyIdValue = transaction.toCurrencyId)?.getOrNull() .toOption() .getOrElse { - Timber.e("Crypto currency not found") + TangemLogger.e("Crypto currency not found") showErrorAlert(OnrampError.DomainError(null)) return@launch } @@ -145,7 +145,7 @@ internal class OnrampSuccessComponentModel @Inject constructor( providerName = transaction.providerName, paymentMethod = transaction.paymentMethod, ) - Timber.e(error.toString()) + TangemLogger.e(error.toString()) showErrorAlert(error) }, ifRight = { status -> diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt index 616f0761c4..8adcb4725d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/SwapSelectTokensController.kt @@ -5,7 +5,7 @@ import com.tangem.features.onramp.swap.entity.utils.createEmptyExchangeTo import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -26,12 +26,12 @@ internal class SwapSelectTokensController @Inject constructor() { ) fun update(transform: (SwapSelectTokensUM) -> SwapSelectTokensUM) { - Timber.d("Applying non-name transformation") + TangemLogger.d("Applying non-name transformation") state.update(transform) } fun update(transformer: SwapSelectTokensUMTransformer) { - Timber.d("Applying ${transformer::class.simpleName ?: "null"}") + TangemLogger.d("Applying ${transformer::class.simpleName ?: "null"}") state.update(transformer::transform) } } \ 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 index 5fb7287648..d2cb9c3ef3 100644 --- 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 @@ -8,7 +8,7 @@ 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 com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -36,17 +36,17 @@ internal class TokenListUMController @Inject constructor() { ) fun update(transform: (TokenListUM) -> TokenListUM) { - Timber.d("Applying non-name transformation") + TangemLogger.d("Applying non-name transformation") state.update(transform) } fun update(transformer: TokenListUMTransformer) { - Timber.d("Applying ${transformer::class.simpleName ?: "unknown"}") + TangemLogger.d("Applying ${transformer::class.simpleName ?: "unknown"}") state.update(transformer::transform) } fun update(transformer: SearchBarUMTransformer) { - Timber.d("Applying ${transformer::class.simpleName ?: "unknown"}") + TangemLogger.d("Applying ${transformer::class.simpleName ?: "unknown"}") state.update { prevState -> prevState.copy( searchBarUM = transformer.transform(prevState.searchBarUM), diff --git a/features/promo-banners/impl/build.gradle.kts b/features/promo-banners/impl/build.gradle.kts index a93ef194f0..d90d0e2f70 100644 --- a/features/promo-banners/impl/build.gradle.kts +++ b/features/promo-banners/impl/build.gradle.kts @@ -36,7 +36,6 @@ dependencies { /** Other */ implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt index 3187d8e91c..5c596f3fd4 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt @@ -12,10 +12,10 @@ import com.tangem.features.promobanners.impl.converters.PromoBannerDisplayToNoti import com.tangem.features.promobanners.impl.repository.PromoBannersRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.logging.TangemLogger import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import java.util.Locale import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -76,7 +76,7 @@ internal class PromoBannersBlockModel @Inject constructor( onCarouselScrolled = ::onCarouselScrolled, ) }.onFailure { error -> - Timber.w(error, "Failed to load promo banners") + TangemLogger.w("Failed to load promo banners", error) } } @@ -111,11 +111,9 @@ internal class PromoBannersBlockModel @Inject constructor( runSuspendCatching { repository.dismissBanner(walletId, displayId) }.onFailure { error -> - Timber.w( + TangemLogger.w( + "Failed to dismiss promo banner $displayId for wallet $walletId", error, - "Failed to dismiss promo banner %s for wallet %s", - displayId, - walletId, ) } } diff --git a/features/push-notifications/impl/build.gradle.kts b/features/push-notifications/impl/build.gradle.kts index e8a673832e..1765878318 100644 --- a/features/push-notifications/impl/build.gradle.kts +++ b/features/push-notifications/impl/build.gradle.kts @@ -23,7 +23,6 @@ dependencies { implementation(deps.compose.accompanist.permission) /** Other dependencies */ - implementation(deps.timber) implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) @@ -34,6 +33,7 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.analytics) implementation(projects.core.analytics.models) + implementation(projects.core.utils) /** Common modules */ implementation(projects.common.routing) diff --git a/features/qr-scanning/impl/build.gradle.kts b/features/qr-scanning/impl/build.gradle.kts index 5e1a8a4c8f..9c92b28a75 100644 --- a/features/qr-scanning/impl/build.gradle.kts +++ b/features/qr-scanning/impl/build.gradle.kts @@ -60,6 +60,5 @@ dependencies { /** Other dependencies */ implementation(deps.arrow.core) - implementation(deps.timber) implementation(tangemDeps.card.core) } \ No newline at end of file diff --git a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt index 8ee7c7ea48..adf60baa52 100644 --- a/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt +++ b/features/qr-scanning/impl/src/main/java/com/tangem/feature/qrscanning/DefaultQrScanningComponent.kt @@ -26,7 +26,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.delay -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.io.IOException import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -63,8 +63,8 @@ class DefaultQrScanningComponent @AssistedInject constructor( } } - val galleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { - val selectedImage = it ?: Uri.EMPTY + val galleryLauncher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + val selectedImage = uri ?: Uri.EMPTY if (selectedImage != Uri.EMPTY) { val mimeType = context.contentResolver.getType(selectedImage) if (mimeType.isImageMimeType()) { @@ -72,7 +72,7 @@ class DefaultQrScanningComponent @AssistedInject constructor( val image = InputImage.fromFilePath(context, selectedImage) analyzer.analyze(image) } catch (e: IOException) { - Timber.e(e, "Unable to get image $selectedImage from gallery") + TangemLogger.e("Unable to get image $selectedImage from gallery", e) } } } diff --git a/features/referral/domain/build.gradle.kts b/features/referral/domain/build.gradle.kts index 3604d3a6be..e9ab3a55d8 100644 --- a/features/referral/domain/build.gradle.kts +++ b/features/referral/domain/build.gradle.kts @@ -35,7 +35,6 @@ dependencies { /** Dependencies */ implementation(deps.arrow.core) implementation(deps.jodatime) - implementation(deps.timber) implementation(tangemDeps.card.core) /** DI */ diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 5e67063fe0..3dac39dddb 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -13,7 +13,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.referral.domain.errors.ReferralError import com.tangem.feature.referral.domain.models.ReferralData import com.tangem.feature.referral.domain.models.TokenData -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class ReferralInteractorImpl( private val repository: ReferralRepository, @@ -62,7 +62,7 @@ internal class ReferralInteractorImpl( ) .mapLeft { it.mapToDomainError() } .onLeft { error -> - Timber.e(error) + TangemLogger.e("Error", error) if (error is ReferralError.UserCancelledException) { throw error } diff --git a/features/referral/impl/build.gradle.kts b/features/referral/impl/build.gradle.kts index 1ced33e61d..8f23358f8f 100644 --- a/features/referral/impl/build.gradle.kts +++ b/features/referral/impl/build.gradle.kts @@ -58,7 +58,6 @@ dependencies { implementation(deps.kotlin.immutable.collections) implementation(deps.decompose.ext.compose) implementation(deps.compose.accompanist.systemUiController) - implementation(deps.timber) /** DI */ implementation(deps.hilt.android) diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/deeplink/DefaultReferralDeepLinkHandler.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/deeplink/DefaultReferralDeepLinkHandler.kt index cd54552b79..ce206c8140 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/deeplink/DefaultReferralDeepLinkHandler.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/deeplink/DefaultReferralDeepLinkHandler.kt @@ -8,7 +8,7 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DefaultReferralDeepLinkHandler @AssistedInject constructor( appRouter: AppRouter, @@ -19,7 +19,7 @@ internal class DefaultReferralDeepLinkHandler @AssistedInject constructor( // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet getSelectedWalletSyncUseCase().fold( ifLeft = { - Timber.e("Error on getting user wallet: $it") + TangemLogger.e("Error on getting user wallet: $it") }, ifRight = { userWallet -> if (userWallet !is UserWallet.Cold || userWallet.cardTypesResolver.isTangemWallet()) { diff --git a/features/send-v2/impl/build.gradle.kts b/features/send-v2/impl/build.gradle.kts index 66e97a0ebc..81c2490591 100644 --- a/features/send-v2/impl/build.gradle.kts +++ b/features/send-v2/impl/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(projects.core.configToggles) implementation(projects.core.navigation) implementation(projects.core.datasource) + implementation(projects.core.utils) api(projects.core.pagination) /** Tangem SDK */ @@ -85,7 +86,6 @@ dependencies { /** Other dependencies */ implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(deps.androidx.paging.runtime) /** DI */ diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt index 71f276e7e4..dec24258e2 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/deeplink/DefaultSellRedirectDeepLinkHandler.kt @@ -18,7 +18,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Suppress("ComplexCondition") internal class DefaultSellRedirectDeepLinkHandler @AssistedInject constructor( @@ -40,13 +40,13 @@ internal class DefaultSellRedirectDeepLinkHandler @AssistedInject constructor( getSelectedWalletSyncUseCase() .fold( ifLeft = { - Timber.e("Error on getting user wallet: $it") + TangemLogger.e("Error on getting user wallet: $it") }, ifRight = { userWallet -> if (currencyId.isNullOrEmpty() || transactionId.isNullOrEmpty() || amount.isNullOrEmpty() || destinationAddress.isNullOrEmpty() ) { - Timber.e( + TangemLogger.e( """ Invalid parameters for SELL deeplink |- Params: $queryParams @@ -57,7 +57,7 @@ internal class DefaultSellRedirectDeepLinkHandler @AssistedInject constructor( scope.launch { val cryptoCurrency = getCryptoCurrency(userWallet.walletId, currencyId).getOrElse { - Timber.e("Error on getting cryptoCurrency: $currencyId") + TangemLogger.e("Error on getting cryptoCurrency: $currencyId") return@launch } // Convert using universal parser to account for regional separators diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt index 836b3534d5..ac7220cf8e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt @@ -43,7 +43,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Suppress("LongParameterList") internal class FeeSelectorLogic @AssistedInject constructor( @@ -277,7 +277,7 @@ internal class FeeSelectorLogic @AssistedInject constructor( val selectedToken = getSelectedTokenStatus(fee.feeTokenId).bind() val availableTokens = getAvailableFeeTokens().fold( ifLeft = { error -> - Timber.e("Failed to get available fee tokens: $error") + TangemLogger.e("Failed to get available fee tokens: $error") if (selectedToken.currency !is CryptoCurrency.Coin) { raise(error) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 710868cdef..4c9a5154ca 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -77,7 +77,7 @@ import com.tangem.utils.extensions.stripZeroPlainString import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import javax.inject.Inject import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned @@ -374,7 +374,7 @@ internal class SendConfirmModel @Inject constructor( network = cryptoCurrency.network, ).fold( ifLeft = { error -> - Timber.e(error) + TangemLogger.e("Error", error) _uiState.update(SendConfirmSendingStateTransformer(isSending = false)) alertFactory.getGenericErrorState( onFailedTxEmailClick = { @@ -470,7 +470,7 @@ internal class SendConfirmModel @Inject constructor( val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency, network) manageCryptoCurrenciesUseCase(accountId = accountId, add = tokenToAdd) - .onLeft(Timber::e) + .onLeft { TangemLogger.e("Error", it) } } } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index d6bbf30296..49db195ff3 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -65,7 +65,7 @@ import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import kotlin.properties.Delegates @@ -262,7 +262,7 @@ internal class SendModel @Inject constructor( } override fun onError(error: GetUserWalletError) { - Timber.w(error.toString()) + TangemLogger.w(error.toString()) showAlertError() } @@ -421,7 +421,7 @@ internal class SendModel @Inject constructor( .launchIn(modelScope) }, ifLeft = { error -> - Timber.w(error.toString()) + TangemLogger.w(error.toString()) showAlertError() return@launch }, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index 084f793b58..b0a07a0db9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -57,7 +57,7 @@ import com.tangem.utils.extensions.stripZeroPlainString import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import javax.inject.Inject import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned @@ -273,7 +273,7 @@ internal class NFTSendConfirmModel @Inject constructor( network = cryptoCurrency.network, ).fold( ifLeft = { error -> - Timber.e(error) + TangemLogger.e("Error", error) _uiState.update(NFTSendConfirmSendingStateTransformer(isSending = false)) alertFactory.getGenericErrorState( onFailedTxEmailClick = { onFailedTxEmailClick(error.localizedMessage.orEmpty()) }, @@ -297,7 +297,7 @@ internal class NFTSendConfirmModel @Inject constructor( result.fold( ifLeft = { error -> - Timber.e(error.toString()) + TangemLogger.e(error.toString()) alertFactory.getSendTransactionErrorState( error = error, popBack = appRouter::pop, diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index 72ddfd69ee..f2d9d8b1cf 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -23,7 +23,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.lifecycle.compose) implementation(deps.jodatime) - implementation(deps.timber) implementation(deps.moshi) /** Compose */ diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt index 9c90294738..c861dfb887 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt @@ -17,7 +17,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Suppress("LongParameterList") internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( @@ -44,7 +44,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( // If selected user wallet is different than from deeplink - ignore deeplink // If selected user wallet is null - ignore deeplink if (walletId != selectedUserWalletId || selectedUserWalletId == null) { - Timber.e("Error on getting user wallet") + TangemLogger.e("Error on getting user wallet") return } @@ -60,7 +60,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( } if (cryptoCurrency == null) { - Timber.e( + TangemLogger.e( """ Could not get crypto currency for |- $NETWORK_ID_KEY: $networkId @@ -77,7 +77,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( val option = (availability as? StakingAvailability.Available)?.option if (option == null) { - Timber.e("Staking is unavailable for ${cryptoCurrency.name}") + TangemLogger.e("Staking is unavailable for ${cryptoCurrency.name}") return@launch } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 27bf32458e..a45bcd44b5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -106,7 +106,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject @@ -835,7 +835,7 @@ internal class StakingModel @Inject constructor( userWalletId = userWalletId, ).fold( ifLeft = { error -> - Timber.e(error.toString()) + TangemLogger.e(error.toString()) analyticsEventHandler.send( StakingAnalyticsEvent.TransactionError( errorCode = "CreateApprovalTxError", @@ -864,7 +864,7 @@ internal class StakingModel @Inject constructor( network = tokenCryptoCurrency.network, ).fold( ifLeft = { error -> - Timber.e(error.toString()) + TangemLogger.e(error.toString()) analyticsEventHandler.send( StakingAnalyticsEvent.TransactionError( errorCode = error.getAnalyticsDescription(), @@ -1438,7 +1438,7 @@ internal class StakingModel @Inject constructor( userWalletId = userWalletId, network = cryptoCurrencyStatus.currency.network, ).getOrElse { throwable -> - Timber.e(throwable) + TangemLogger.e("Error", throwable) false } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakeKitTransactionSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakeKitTransactionSender.kt index c0e37364e6..ca9f4e5992 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakeKitTransactionSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakeKitTransactionSender.kt @@ -39,7 +39,7 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal @Suppress("LongParameterList") @@ -286,7 +286,7 @@ internal class StakeKitTransactionSender @AssistedInject constructor( transactionHash = transactionHash, ) }.onRight { - Timber.d("Successful hash submission") + TangemLogger.d("Successful hash submission") } } } diff --git a/features/stories/impl/build.gradle.kts b/features/stories/impl/build.gradle.kts index 077cadf096..bb49f8b167 100644 --- a/features/stories/impl/build.gradle.kts +++ b/features/stories/impl/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.res) implementation(projects.core.ui) + implementation(projects.core.utils) implementation(projects.core.analytics) /** AndroidX */ @@ -37,10 +38,9 @@ dependencies { implementation(deps.compose.ui.tooling) /** Others */ - implementation(deps.kotlin.immutable.collections) - implementation(deps.compose.coil) - implementation(deps.timber) implementation(deps.arrow.core) + implementation(deps.compose.coil) + implementation(deps.kotlin.immutable.collections) /** DI */ implementation(deps.hilt.android) diff --git a/features/stories/impl/src/main/java/com/tangem/feature/stories/impl/model/StoriesModel.kt b/features/stories/impl/src/main/java/com/tangem/feature/stories/impl/model/StoriesModel.kt index 875871e2cd..35c985a28a 100644 --- a/features/stories/impl/src/main/java/com/tangem/feature/stories/impl/model/StoriesModel.kt +++ b/features/stories/impl/src/main/java/com/tangem/feature/stories/impl/model/StoriesModel.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject internal class StoriesModel @Inject constructor( @@ -52,7 +52,7 @@ internal class StoriesModel @Inject constructor( modelScope.launch { getStoryContentUseCase.invokeSync(params.storyId).fold( ifLeft = { - Timber.e("Unable to load stories for ${params.storyId}") + TangemLogger.e("Unable to load stories for ${params.storyId}") openScreen(hideStories = false) }, ifRight = { story -> diff --git a/features/swap-v2/impl/build.gradle.kts b/features/swap-v2/impl/build.gradle.kts index b64e7ab8d8..612a39dcc8 100644 --- a/features/swap-v2/impl/build.gradle.kts +++ b/features/swap-v2/impl/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(projects.core.configToggles) implementation(projects.core.datasource) implementation(projects.core.analytics) + implementation(projects.core.utils) /** Common */ implementation(projects.common.ui) @@ -85,7 +86,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.decompose) implementation(deps.decompose.ext.compose) - implementation(deps.timber) implementation(deps.kotlin.immutable.collections) implementation(deps.coil) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 2b3fc14c6d..45499a10b7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -63,7 +63,7 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.util.Locale import javax.inject.Inject @@ -558,7 +558,7 @@ internal class SwapAmountModel @Inject constructor( startLoadingQuotesTask(isSilentReload = false) } else { @Suppress("NullableToStringCall") - Timber.e( + TangemLogger.e( """ Invalid cryptocurrencies status: | Primary -> $primaryStatus diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt index a9312fc84a..b40d57b863 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/SwapChooseTokenNetworkModel.kt @@ -28,7 +28,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -67,7 +67,7 @@ internal class SwapChooseTokenNetworkModel @Inject constructor( private fun initContent() { val userWallet = getUserWalletUseCase(params.userWalletId).getOrElse { error -> - Timber.e("Failed to get user wallet: $error") + TangemLogger.e("Failed to get user wallet: $error") swapChooseTokenAlertFactory.getGenericErrorState(params.onDismiss) return } @@ -77,7 +77,7 @@ internal class SwapChooseTokenNetworkModel @Inject constructor( token = params.token, userWalletId = params.userWalletId, ).getOrElse { - Timber.e("Failed to get crypto currency") + TangemLogger.e("Failed to get crypto currency") swapChooseTokenAlertFactory.getGenericErrorState(params.onDismiss) return@launch } @@ -88,7 +88,7 @@ internal class SwapChooseTokenNetworkModel @Inject constructor( filterProviderTypes = SEND_WITH_SWAP_PROVIDER_TYPES, swapTxType = SwapTxType.SendWithSwap, ).getOrElse { error -> - Timber.e(error.toString()) + TangemLogger.e(error.toString()) uiState.update( SwapChooseErrorStateTransformer( tokenName = params.token.name, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt index 41f709edb1..b5c91be315 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt @@ -28,7 +28,7 @@ import com.tangem.features.swap.v2.impl.common.ConfirmData import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.math.RoundingMode @@ -67,7 +67,7 @@ internal class SwapTransactionSender @AssistedInject constructor( ExpressProviderType.DEX_BRIDGE, ExpressProviderType.ONRAMP, -> { - Timber.w("Provider $providerType is not supported in Send With Swap") + TangemLogger.w("Provider $providerType is not supported in Send With Swap") onExpressError(ExpressError.UnknownError) } } @@ -155,7 +155,7 @@ internal class SwapTransactionSender @AssistedInject constructor( userWalletId = userWallet.walletId, network = fromStatus.currency.network, ).getOrElse { error -> - Timber.e(error, "Failed to create swap CEX tx data") + TangemLogger.e("Failed to create swap CEX tx data", error) onSendError(SendTransactionError.UnknownError(Exception(error))) return } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt index 039d065657..2ebed7f554 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt @@ -34,7 +34,7 @@ import com.tangem.features.swap.v2.impl.sendviaswap.entity.SendWithSwapUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import kotlin.properties.Delegates @@ -159,7 +159,7 @@ internal class SendWithSwapModel @Inject constructor( getPrimaryCurrencyStatusUpdates(params.currency) }, ifLeft = { error -> - Timber.w(error.toString()) + TangemLogger.w(error.toString()) swapAlertFactory.getGenericErrorState( expressError = ExpressError.UnknownError, onFailedTxEmailClick = { diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index 4efc95da23..dd4eda6edb 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -54,7 +54,6 @@ dependencies { implementation(tangemDeps.blockchain) /** Others */ - implementation(deps.timber) implementation(deps.jodatime) /** DI */ diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index 7e3ef9caeb..ebd13fd35b 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -39,7 +39,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.io.IOException import java.math.BigDecimal import java.util.UUID @@ -253,7 +253,7 @@ internal class DefaultSwapRepository( ) }, catch = { exception -> - Timber.e("getExchangeStatus error: $exception") + TangemLogger.e("getExchangeStatus error: $exception") raise(UnknownError(exception.message)) }, ) @@ -404,7 +404,7 @@ internal class DefaultSwapRepository( return try { txDetailsMoshiAdapter.fromJson(txDetailsJson) } catch (e: IOException) { - Timber.e(e, "error parsing txDetailsJson") + TangemLogger.e("error parsing txDetailsJson", e) null } } diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index 02e0992d2b..d0baa14afb 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -55,7 +55,6 @@ dependencies { /** Other Libraries **/ implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) - implementation(deps.timber) implementation(tangemDeps.blockchain) implementation(tangemDeps.card.core) implementation(deps.moshi) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 3b7b8442ab..96f8ec12bf 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -70,7 +70,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.coroutineScope -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.math.BigInteger import java.math.RoundingMode @@ -290,7 +290,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( contractAddress = permissionOptions.forTokenContractAddress, spenderAddress = permissionOptions.spenderAddress, ).getOrElse { error -> - Timber.e(error, "Failed to create approveTransaction") + TangemLogger.e("Failed to create approveTransaction", error) return SwapTransactionState.Error.UnknownError } @@ -321,7 +321,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( reduceBalanceBy: BigDecimal, txFeeSealedState: TxFeeSealedState, ): Map { - Timber.i( + TangemLogger.i( """ Find the best quote |- fromToken: $fromToken @@ -702,7 +702,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( expressOperationType: ExpressOperationType, isTangemPayWithdrawal: Boolean, ): SwapTransactionState { - Timber.i( + TangemLogger.i( """ Swap |- swapProvider: $swapProvider @@ -795,7 +795,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( network = currencyToSendStatus.currency.network, txExtras = createDexTxExtras(dataToSign, currencyToSendStatus.currency.network, txFee.fee.getGasLimit()), ).getOrElse { error -> - Timber.e(error, "Failed to create swap dex tx data") + TangemLogger.e("Failed to create swap dex tx data", error) return SwapTransactionState.Error.UnknownError } @@ -999,7 +999,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( userWalletId = userWalletId, network = currencyToSend.currency.network, ).getOrElse { error -> - Timber.e(error, "Failed to create swap CEX tx data") + TangemLogger.e("Failed to create swap CEX tx data", error) return SwapTransactionState.Error.UnknownError } @@ -2065,7 +2065,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( userWallet = userWallet, ).getOrNull() ?: error("unable to calculate fee") } catch (e: Exception) { - Timber.e(e, "Failed to get fee") + TangemLogger.e("Failed to get fee", e) // it's impossible next steps without fee return createSwapErrorWith( fromToken = fromTokenStatus, @@ -2491,7 +2491,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = this@getQuotesOrEmpty).orEmpty() }.getOrElse { e -> - Timber.e(e, "Failed to get quotes: ${e.message.orEmpty()}") + TangemLogger.e("Failed to get quotes: ${e.message.orEmpty()}", e) emptySet() } } @@ -2505,7 +2505,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( return try { SolanaTransactionHelper.removeSignaturesPlaceholders(hash) } catch (e: Exception) { - Timber.e("Failed to format the hash: ${e.message.orEmpty()}") + TangemLogger.e("Failed to format the hash: ${e.message.orEmpty()}") hash } } diff --git a/features/swap/impl/build.gradle.kts b/features/swap/impl/build.gradle.kts index 8a54dbba3d..297b57d054 100644 --- a/features/swap/impl/build.gradle.kts +++ b/features/swap/impl/build.gradle.kts @@ -94,7 +94,6 @@ dependencies { implementation(deps.compose.accompanist.systemUiController) implementation(deps.kotlin.serialization) implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(deps.decompose.ext.compose) /** Tangem libs */ diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 6fd3103a0f..8b516985a2 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -115,7 +115,7 @@ import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.math.RoundingMode import java.text.DecimalFormat @@ -555,7 +555,7 @@ internal class SwapModel @Inject constructor( subscribeToCoinBalanceUpdatesIfNeeded() }.onFailure { error -> - Timber.e(error) + TangemLogger.e("Error", error) applyInitialTokenChoice( state = TokensDataStateExpress.EMPTY, @@ -597,7 +597,7 @@ internal class SwapModel @Inject constructor( swapRouter.back() }.onFailure { error -> - Timber.e(error) + TangemLogger.e("Error", error) } } } @@ -820,11 +820,11 @@ internal class SwapModel @Inject constructor( } } else { feeSelectorRepository.state.value = FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true) - Timber.e("Accidentally empty quotes list") + TangemLogger.e("Accidentally empty quotes list") } }, onError = { error -> - Timber.e("Error when loading quotes: $error") + TangemLogger.e("Error when loading quotes: $error") feeSelectorRepository.state.value = FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true) uiState = stateBuilder.addNotification(uiState, null) { startLoadingQuotesFromLastState() } }, @@ -1031,7 +1031,7 @@ internal class SwapModel @Inject constructor( val provider = requireNotNull(dataState.selectedProvider) { "Selected provider is null" } val lastLoadedQuotesState = dataState.lastLoadedSwapStates[provider] as? SwapState.QuotesLoadedState if (lastLoadedQuotesState == null) { - Timber.e("Last loaded quotes state is null") + TangemLogger.e("Last loaded quotes state is null") return } val fromCurrency = requireNotNull(dataState.fromCryptoCurrency) @@ -1075,7 +1075,7 @@ internal class SwapModel @Inject constructor( txHash = swapTransactionState.txHash, currency = fromCurrency.currency, ).getOrElse { - Timber.i("tx hash explore not supported") + TangemLogger.i("tx hash explore not supported") "" } @@ -1119,7 +1119,7 @@ internal class SwapModel @Inject constructor( } } }.onFailure { error -> - Timber.e(error) + TangemLogger.e("Error", error) startLoadingQuotesFromLastState() showAlert() } @@ -1214,7 +1214,7 @@ internal class SwapModel @Inject constructor( val feeForPermission = when (val fee = approveDataModel.fee) { TxFeeState.Empty -> { showAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) - Timber.e("Fee should not be Empty") + TangemLogger.e("Fee should not be Empty") return@launch } is TxFeeState.MultipleFeeState -> fee.priorityFee @@ -1254,7 +1254,7 @@ internal class SwapModel @Inject constructor( } }.onFailure { showAlert() } }.onFailure { error -> - Timber.e(error.message.orEmpty()) + TangemLogger.e(error.message.orEmpty()) showAlert() } } @@ -1436,14 +1436,14 @@ internal class SwapModel @Inject constructor( coin: CryptoCurrency.Coin, isFromCurrency: Boolean, ) { - Timber.d("Subscribe to ${coin.id} balance updates") + TangemLogger.d("Subscribe to ${coin.id} balance updates") getAccountCurrencyStatusUseCase( userWalletId = userWalletId, currency = coin, ).distinctUntilChanged { old, new -> old.status.value.amount == new.status.value.amount } // Check only balance changes .onEach { (account, currencyStatus) -> - Timber.d("${coin.id} balance is ${currencyStatus.value.amount ?: "null"}") + TangemLogger.d("${coin.id} balance is ${currencyStatus.value.amount ?: "null"}") if (isFromCurrency) { dataState = dataState.copy( diff --git a/features/tangempay/details/impl/build.gradle.kts b/features/tangempay/details/impl/build.gradle.kts index cc18ae7c58..0aee66703c 100644 --- a/features/tangempay/details/impl/build.gradle.kts +++ b/features/tangempay/details/impl/build.gradle.kts @@ -20,6 +20,7 @@ dependencies { implementation(projects.core.error) implementation(projects.core.navigation) implementation(projects.core.ui) + implementation(projects.core.utils) /** Common */ implementation(projects.common.ui) @@ -60,5 +61,4 @@ dependencies { /** Other */ implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt index 634b5a0e69..de7acccebe 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayChangePinModel.kt @@ -21,7 +21,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Stable @@ -57,7 +57,7 @@ internal class TangemPayChangePinModel @Inject constructor( pin = uiState.value.pinCode, ).getOrNull() } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) return@launch } uiState.update { it.copy(submitButtonLoading = false) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index 4ada6b4b01..cc3b8be977 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -62,7 +62,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -181,7 +181,7 @@ internal class TangemPayDetailsModel @Inject constructor( val result = try { cardDetailsRepository.freezeCard(userWalletId = params.userWalletId, cardId = params.config.cardId) } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) return@launch } result @@ -220,7 +220,7 @@ internal class TangemPayDetailsModel @Inject constructor( val result = try { cardDetailsRepository.unfreezeCard(userWalletId = params.userWalletId, cardId = params.config.cardId) } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) return@launch } result @@ -333,7 +333,7 @@ internal class TangemPayDetailsModel @Inject constructor( val result = try { cardDetailsRepository.getCardBalance(params.userWalletId).onRight { balance = it } } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) return@launch } uiState.update( @@ -357,7 +357,7 @@ internal class TangemPayDetailsModel @Inject constructor( val isDone = try { cardDetailsRepository.isAddToWalletDone(params.userWalletId).getOrNull() == true } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) return@launch } uiState.update( @@ -404,7 +404,7 @@ internal class TangemPayDetailsModel @Inject constructor( try { cardDetailsRepository.setAddToWalletAsDone(params.userWalletId) } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) } uiState.update( transformer = DetailsAddToWalletBannerTransformer( diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/GoogleWalletUtil.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/GoogleWalletUtil.kt index 22e3cf50fe..51aa84aae6 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/GoogleWalletUtil.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/GoogleWalletUtil.kt @@ -3,7 +3,7 @@ package com.tangem.features.tangempay.utils import android.content.Context import android.content.Intent import dagger.hilt.android.qualifiers.ApplicationContext -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val TAG = "GoogleWalletUtil" @@ -24,15 +24,16 @@ internal class GoogleWalletUtil @Inject constructor( } private fun getWalletIntent(): Intent? { - return if (walletIntent != null) { - walletIntent + val cached = walletIntent + return if (cached != null) { + cached } else { try { context.packageManager.getLaunchIntentForPackage(WALLET_PACKAGE_NAME) ?.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } .also { walletIntent = it } } catch (exception: Exception) { - Timber.tag(TAG).e(exception) + TangemLogger.withTag(TAG).e("Error", exception) null } } diff --git a/features/tangempay/onboarding/impl/build.gradle.kts b/features/tangempay/onboarding/impl/build.gradle.kts index c94b59ab29..5762f030f4 100644 --- a/features/tangempay/onboarding/impl/build.gradle.kts +++ b/features/tangempay/onboarding/impl/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation(projects.core.error) implementation(projects.core.navigation) implementation(projects.core.ui) + implementation(projects.core.utils) /** Common */ implementation(projects.common.routing) @@ -49,7 +50,6 @@ dependencies { kapt(deps.hilt.kapt) /** Other */ - implementation(deps.timber) implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt index dadd979f7f..78a7228a9d 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -25,11 +25,11 @@ import com.tangem.features.tangempay.model.transformers.TangemPayOnboardingButto import com.tangem.features.tangempay.ui.TangemPayOnboardingNavigation import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject import com.tangem.utils.transformer.update as transformerUpdate @@ -100,7 +100,7 @@ internal class TangemPayOnboardingModel @Inject constructor( if (customerInfo.productInstance == null) { repository.createOrder(userWalletId) .onLeft { error -> - Timber.e("Error creating order before KYC: $error") + TangemLogger.e("Error creating order before KYC: $error") } } when (params) { @@ -170,14 +170,14 @@ internal class TangemPayOnboardingModel @Inject constructor( val result = produceInitialDataUseCase(userWalletId) if (result.isLeft()) { val errorMessage = result.leftOrNull()?.message ?: "Unknown error" - Timber.e("Error producing initial data: $errorMessage") + TangemLogger.e("Error producing initial data: $errorMessage") uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) return@launch } repository.getCustomerInfo(userWalletId = userWalletId) .fold( ifLeft = { error -> - Timber.e("Error getCustomerInfo: ${error.errorCode}") + TangemLogger.e("Error getCustomerInfo: ${error.errorCode}") uiState.transformerUpdate(TangemPayOnboardingButtonLoadingTransformer(isLoading = false)) }, ifRight = { customerInfo -> @@ -187,7 +187,7 @@ internal class TangemPayOnboardingModel @Inject constructor( if (customerInfo.productInstance == null) { repository.createOrder(userWalletId) .onLeft { error -> - Timber.e("Error creating order before KYC: $error") + TangemLogger.e("Error creating order before KYC: $error") } } openKyc(userWalletId) diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 6554b8a54f..3c61c2ed70 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -48,7 +48,6 @@ dependencies { /** Other libraries */ implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(deps.surveysparrow) /** Core modules */ diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt index f19b69a825..bad48fc458 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt @@ -26,7 +26,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.feature.tester.impl.R import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.HideAllCurrenciesUM import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.ToggleAppThemeUM -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.io.File @OptIn(ExperimentalFoundationApi::class) @@ -98,7 +98,7 @@ private fun Activity.shareFile(file: File?) { ContextCompat.startActivity(this, chooserIntent, null) } catch (ex: Exception) { - Timber.e("Failed to share file: $ex") + TangemLogger.e("Failed to share file: $ex") } } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt index 599f82475f..5eeb879082 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @HiltViewModel @@ -86,7 +86,7 @@ internal class TesterActionsViewModel @Inject constructor( AppThemeMode.FOLLOW_SYSTEM -> AppThemeMode.FORCE_DARK } - Timber.d( + TangemLogger.d( """ Change app theme mode |- Current theme mode: $currentAppThemeMode @@ -95,7 +95,7 @@ internal class TesterActionsViewModel @Inject constructor( ) changeAppThemeModeUseCase(newAppThemeMode).onLeft { error -> - Timber.e( + TangemLogger.e( """ Unable to change app theme mode |- Error: $error @@ -108,7 +108,7 @@ internal class TesterActionsViewModel @Inject constructor( getAppThemeModeUseCase() .distinctUntilChanged() .onEach { maybeAppThemeMode -> - Timber.d( + TangemLogger.d( """ Current app theme mode updated |- Previous app theme mode: ${uiState.toggleAppThemeUM.currentAppTheme} @@ -119,7 +119,7 @@ internal class TesterActionsViewModel @Inject constructor( uiState = uiState.copy( toggleAppThemeUM = uiState.toggleAppThemeUM.copy( currentAppTheme = maybeAppThemeMode.getOrElse { error -> - Timber.e( + TangemLogger.e( """ Unable to get current app theme mode, using default |- Default theme mode: ${AppThemeMode.DEFAULT} diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt index 5a91649f19..3b2385900c 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt @@ -3,7 +3,7 @@ package com.tangem.feature.tester.presentation.surveysparrow import android.app.Activity import com.surveysparrow.ss_android_sdk.SsSurvey import com.surveysparrow.ss_android_sdk.SurveySparrow -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Manager for Survey Sparrow SDK. @@ -33,7 +33,7 @@ class SurveySparrowManager( SurveySparrow(activity, survey) } catch (e: Exception) { - Timber.e(e, "Failed to create SurveySparrow survey") + TangemLogger.e("Failed to create SurveySparrow survey", e) null } } @@ -49,7 +49,7 @@ class SurveySparrowManager( val surveySparrow = createSurvey(activity, customVariables) if (surveySparrow != null) { surveySparrow.startSurveyForResult(requestCode) - Timber.d("SurveySparrow survey started with requestCode: $requestCode") + TangemLogger.d("SurveySparrow survey started with requestCode: $requestCode") } } } \ No newline at end of file diff --git a/features/token-recieve/impl/build.gradle.kts b/features/token-recieve/impl/build.gradle.kts index b1b8d7b048..ddd6c0d9f9 100644 --- a/features/token-recieve/impl/build.gradle.kts +++ b/features/token-recieve/impl/build.gradle.kts @@ -23,7 +23,6 @@ dependencies { implementation(deps.compose.ui.utils) implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(deps.lifecycle.compose) implementation(deps.kotlin.serialization) implementation(deps.decompose.ext.compose) diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 3b4adc992f..51a9203ba8 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -37,7 +37,6 @@ dependencies { implementation(deps.reKotlin) implementation(tangemDeps.blockchain) implementation(tangemDeps.card.core) - implementation(deps.timber) implementation(deps.lifecycle.compose) implementation(deps.kotlin.serialization) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt index 20cf144c6d..d375886c77 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt @@ -31,7 +31,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Suppress("LongParameterList") internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( @@ -66,18 +66,18 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( val userWallet = userWalletId?.let { getUserWalletUseCase(userWalletId) }?.getOrNull() // If wallet to select is null or locked, ignore deeplink if (userWallet == null || userWallet.isLocked) { - Timber.e("Error on getting user wallet") + TangemLogger.e("Error on getting user wallet") return@launch } if (selectWalletUseCase(userWalletId).getOrNull() == null) { - Timber.e("Error on selecting user wallet") + TangemLogger.e("Error on selecting user wallet") return@launch } val cryptoCurrency = findCryptoCurrency(userWallet = userWallet, networkId = networkId, tokenId = tokenId) if (cryptoCurrency == null) { - Timber.e( + TangemLogger.e( """ Could not get crypto currency for |- $NETWORK_ID_KEY: $networkId diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index d260d470a2..e0034a4926 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import arrow.core.getOrElse import arrow.core.merge import arrow.core.right +import com.tangem.utils.logging.TangemLogger import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss @@ -103,7 +104,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList", "LargeClass", "TooManyFunctions", "PropertyUsedBeforeDeclaration") @@ -499,11 +499,9 @@ internal class TokenDetailsModel @Inject constructor( ), ) - Timber.e( - /* t = */ throwable, - /* message = */ "Unable to get wallet manager for user wallet %s and network %s", - /* ...args = */ userWalletId, - cryptoCurrency.network, + TangemLogger.e( + "Unable to get wallet manager for user wallet $userWalletId and network ${cryptoCurrency.network}", + throwable, ) false @@ -655,7 +653,7 @@ internal class TokenDetailsModel @Inject constructor( cryptoCurrency.network, ).fold( ifLeft = { throwable -> - Timber.e(throwable.cause?.localizedMessage.orEmpty()) + TangemLogger.e(throwable.cause?.localizedMessage.orEmpty()) "" }, ifRight = { it }, @@ -756,12 +754,12 @@ internal class TokenDetailsModel @Inject constructor( val accountId = account?.accountId if (accountId == null) { - Timber.e("Account ID is null, cannot hide currency ${cryptoCurrency.id}") + TangemLogger.e("Account ID is null, cannot hide currency ${cryptoCurrency.id}") return@launch } manageCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrency) - .onLeft { Timber.e(it) } + .onLeft { TangemLogger.e("Error", it) } .onRight { router.popBackStack() } } } @@ -830,7 +828,7 @@ internal class TokenDetailsModel @Inject constructor( txHash = txHash, currency = cryptoCurrency, ).fold( - ifLeft = { Timber.e(it.toString()) }, + ifLeft = { TangemLogger.e(it.toString()) }, ifRight = { router.openUrl(url = it) }, ) } @@ -960,7 +958,7 @@ internal class TokenDetailsModel @Inject constructor( } if (message != null) { internalUiState.value = stateFactory.getStateWithErrorDialog(stringReference(message)) - Timber.e(message) + TangemLogger.e(message) } }, ifRight = { @@ -1033,7 +1031,7 @@ internal class TokenDetailsModel @Inject constructor( internalUiState.value = stateFactory.getStateWithErrorDialog( stringReference(e.message.orEmpty()), ) - Timber.e(e.message) + TangemLogger.e("Error: $e") }, ifRight = { internalUiState.value = stateFactory.getStateWithRemovedKaspaIncompleteTransactionNotification() @@ -1068,7 +1066,7 @@ internal class TokenDetailsModel @Inject constructor( internalUiState.value = stateFactory.getStateWithErrorDialog( stringReference(e.message.orEmpty()), ) - Timber.e(e.message) + TangemLogger.e("Error: $e") } } }, @@ -1141,7 +1139,7 @@ internal class TokenDetailsModel @Inject constructor( } private fun showStakingUnavailable() { - Timber.e("Staking is unavailable for ${cryptoCurrency.name}") + TangemLogger.e("Staking is unavailable for ${cryptoCurrency.name}") uiMessageSender.send(SnackbarMessage(resourceReference(R.string.staking_error_no_validators_title))) } 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 38588b2f69..a57f83eafb 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 @@ -22,7 +22,7 @@ import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.removeBy import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import kotlin.String @@ -150,7 +150,7 @@ internal class TokenDetailsNotificationConverter( maxManaBalanceAmount = warning.maxAmount?.let { formatMana(it) } ?: run { - Timber.e("FeeResource maxAmount cannot be null in Koinos. Check KoinosWalletManager") + TangemLogger.e("FeeResource maxAmount cannot be null in Koinos. Check KoinosWalletManager") "" }, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt index 4d2e2e0835..61415883f5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStakingInfoConverter.kt @@ -24,7 +24,7 @@ import com.tangem.lib.crypto.BlockchainUtils.isStakingRewardUnavailable import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal internal class TokenDetailsStakingInfoConverter( @@ -46,7 +46,9 @@ internal class TokenDetailsStakingInfoConverter( state: TokenDetailsState, stakingAvailability: StakingAvailability, ): StakingBlockUM? { - Timber.i("Define staking block for [${status.currency.id.value}] with availability:\n$stakingAvailability") + TangemLogger.i( + "Define staking block for [${status.currency.id.value}] with availability:\n$stakingAvailability", + ) return when (stakingAvailability) { StakingAvailability.TemporaryUnavailable -> StakingBlockUM.TemporaryUnavailable StakingAvailability.Unavailable -> null @@ -73,7 +75,7 @@ internal class TokenDetailsStakingInfoConverter( val iconState = state.tokenInfoBlockState.iconState - Timber.i( + TangemLogger.i( """ getStakingInfoBlock: – stakingBalance: ${stakingBalance ?: "null"} 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 c4204e04e1..bc8cbf7110 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 @@ -36,7 +36,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.util.Locale @@ -122,7 +122,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( fun updateTxStatus(tx: ExchangeUM, statusModel: ExchangeStatusModel): ExchangeUM { if (tx.activeStatus == statusModel.status && tx.hasLongTime == statusModel.hasLongTime) { - Timber.e("UpdateTxStatus isn't required. Current status isn't changed") + TangemLogger.e("UpdateTxStatus isn't required. Current status isn't changed") return tx } val hasFailed = statusModel.status.isFailed() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt index 20d13122a3..3bc30b0d7c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt @@ -29,7 +29,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.map -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Suppress("LongParameterList") internal class ExchangeStatusFactory @AssistedInject constructor( @@ -127,7 +127,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( type = provider.type, ) } else { - Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}") + TangemLogger.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}") null } @@ -175,7 +175,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( contractAddress = refundContractAddress, networkId = refundNetwork, ) - .onLeft(Timber::e) + .onLeft { TangemLogger.e("Error", it) } .getOrNull() } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt index cbdf9089fa..60d403d38c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express +import com.tangem.utils.logging.TangemLogger import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState @@ -25,7 +26,6 @@ import dagger.assisted.AssistedInject import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import timber.log.Timber @Suppress("LongParameterList") internal class OnrampStatusFactory @AssistedInject constructor( @@ -83,8 +83,8 @@ internal class OnrampStatusFactory @AssistedInject constructor( onrampTx } else { getOnrampStatusUseCase(userWallet = userWallet, onrampTx.info.txId).fold( - ifLeft = { - Timber.e("Couldn't update onramp status. $it") + ifLeft = { error -> + TangemLogger.e("Couldn't update onramp status. $error") onrampTx }, ifRight = { statusModel -> diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt index d145a7ab35..a8af24a292 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt @@ -29,7 +29,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.map -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import kotlin.coroutines.cancellation.CancellationException @Suppress("LongParameterList") @@ -128,7 +128,7 @@ internal class TokenDetailsExchangeStatusFactory @AssistedInject constructor( type = provider.type, ) } else { - Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}") + TangemLogger.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}") null } @@ -176,7 +176,7 @@ internal class TokenDetailsExchangeStatusFactory @AssistedInject constructor( contractAddress = refundContractAddress, networkId = refundNetwork, ) - .onLeft(Timber::e) + .onLeft { TangemLogger.e("Error", it) } .getOrNull() } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsOnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsOnrampStatusFactory.kt index 802663f78a..f164ef18cb 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsOnrampStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsOnrampStatusFactory.kt @@ -25,7 +25,7 @@ import dagger.assisted.AssistedInject import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Suppress("LongParameterList") internal class TokenDetailsOnrampStatusFactory @AssistedInject constructor( @@ -84,7 +84,7 @@ internal class TokenDetailsOnrampStatusFactory @AssistedInject constructor( } else { getOnrampStatusUseCase(userWallet = userWallet, onrampTx.info.txId).fold( ifLeft = { error -> - Timber.e("Couldn't update onramp status. $error") + TangemLogger.e("Couldn't update onramp status. $error") onrampTx }, ifRight = { statusModel -> diff --git a/features/txhistory/impl/build.gradle.kts b/features/txhistory/impl/build.gradle.kts index aae9596318..3ea746b243 100644 --- a/features/txhistory/impl/build.gradle.kts +++ b/features/txhistory/impl/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) + implementation(projects.core.utils) implementation(projects.common.routing) implementation(projects.core.configToggles) implementation(projects.core.analytics) @@ -57,5 +58,4 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) implementation(deps.decompose.ext.compose) - implementation(deps.timber) } \ No newline at end of file diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt index 7476b496da..7d89a3a061 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt @@ -27,7 +27,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -213,7 +213,7 @@ internal class TxHistoryModel @Inject constructor( txHash = txHash, currency = params.currency, ).fold( - ifLeft = { Timber.e(it.toString()) }, + ifLeft = { TangemLogger.e(it.toString()) }, ifRight = { urlOpener.openUrl(url = it) }, ) } diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index fb82ff618a..634724e099 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.core.configToggles) implementation(projects.core.navigation) implementation(projects.core.analytics) + implementation(projects.core.utils) implementation(projects.core.analytics.models) implementation(projects.core.datasource) implementation(projects.common.routing) @@ -71,7 +72,6 @@ dependencies { /* Other */ implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(deps.reKotlin) /** Tangem libraries */ diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/model/RenameWalletModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/model/RenameWalletModel.kt index ef51de94a8..5da7a76ea4 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/model/RenameWalletModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/model/RenameWalletModel.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -103,7 +103,7 @@ internal class RenameWalletModel @Inject constructor( renameWalletUseCase(userWalletId = params.userWalletId, name = newName.text) .onLeft { error -> - Timber.e("Unable to rename wallet: $error") + TangemLogger.e("Unable to rename wallet: $error") showRenameWalletError(error = error, updatedName = newName.text) } } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 75dac67162..52efecf7c9 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -57,7 +57,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) @@ -250,7 +250,7 @@ internal class WalletSettingsModel @Inject constructor( private fun forgetWallet() = modelScope.launch { val hasUserWallets = deleteWalletUseCase(params.userWalletId).getOrElse { error -> - Timber.e("Unable to delete wallet: $error") + TangemLogger.e("Unable to delete wallet: $error") messageSender.send( message = SnackbarMessage(resourceReference(R.string.common_unknown_error)), @@ -447,7 +447,7 @@ internal class WalletSettingsModel @Inject constructor( -> modelScope.launch { unlockHotWalletContextualUseCase.invoke(hotWalletId) .onLeft { - Timber.e(it, "Unable to unlock wallet with id ${params.userWalletId}") + TangemLogger.e("Unable to unlock wallet with id ${params.userWalletId}", it) } .onRight { action(true) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountListSortingSaver.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountListSortingSaver.kt index f698a67127..bf3acacb9a 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountListSortingSaver.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountListSortingSaver.kt @@ -7,7 +7,7 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration.Companion.seconds @@ -38,7 +38,7 @@ internal class AccountListSortingSaver @Inject constructor( .debounce { 3.seconds } .onEach { accountIds -> applyAccountListSortingUseCase.invoke(accountIds).onLeft { - Timber.e("Error while saving account list sorting: $it") + TangemLogger.e("Error while saving account list sorting: $it") } .onRight { analyticsEventHandler.send(WalletSettingsAnalyticEvents.LongtapAccountsOrder()) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index cb23c78346..7fcdc3dc2e 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -45,7 +45,6 @@ dependencies { implementation(tangemDeps.hot.core) implementation(tangemDeps.card.core) implementation(tangemDeps.blockchain) - implementation(deps.timber) implementation(deps.firebase.perf) { exclude(group = "com.google.firebase", module = "protolite-well-known-types") exclude(group = "com.google.protobuf", module = "protobuf-javalite") diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index e64451ff14..1d3096be23 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -68,7 +68,7 @@ import com.tangem.utils.coroutines.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.* -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val TANGEM_PAY_UPDATE_INTERVAL = 60_000L @@ -157,7 +157,7 @@ internal class WalletModel @Inject constructor( modelScope.launch { bindRefcodeWithWalletUseCase.retry() - .onLeft { Timber.e("Failed to bind refcode with wallets: $it") } + .onLeft { TangemLogger.e("Failed to bind refcode with wallets: $it") } } } @@ -308,7 +308,7 @@ internal class WalletModel @Inject constructor( val shouldAskNotificationPermissionsViaBs = notificationsRepository.shouldAskNotificationPermissionsViaBs() val shouldShow = notificationsRepository.shouldShowSubscribeOnNotificationsAfterUpdate() val isHuaweiDevice = getIsHuaweiDeviceWithoutGoogleServicesUseCase() - Timber.d( + TangemLogger.d( "push BS afterUpdate: $shouldShow," + "isHuaweiDevice $isHuaweiDevice", ) @@ -454,7 +454,7 @@ internal class WalletModel @Inject constructor( awaitAll( async { refreshMultiCurrencyWalletQuotesUseCase(wallet.walletCardState.id).getOrElse { - Timber.e("Failed to refreshMultiCurrencyWalletQuotesUseCase $it") + TangemLogger.e("Failed to refreshMultiCurrencyWalletQuotesUseCase $it") } }, async { @@ -497,10 +497,10 @@ internal class WalletModel @Inject constructor( } is WalletsUpdateActionResolver.Action.ReorderWallets -> reorderWallets(action) WalletsUpdateActionResolver.Action.EmptyWallets -> { - Timber.w("Wallets list is empty!") + TangemLogger.w("Wallets list is empty!") } is WalletsUpdateActionResolver.Action.Unknown -> { - Timber.w("Unable to perform action: $action") + TangemLogger.w("Unable to perform action: $action") } } } @@ -820,7 +820,7 @@ internal class WalletModel @Inject constructor( setNotificationsEnabledUseCase(userWalletId, true).onRight { notificationsRepository.setNotificationsWasEnabledAutomatically(userWalletId.stringValue) }.onLeft { - Timber.e(it) + TangemLogger.e("Error", it) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt index fccfccfd12..501510ac6a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt @@ -13,7 +13,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificat import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletType -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -42,7 +42,7 @@ internal class WalletsUpdateActionResolver @Inject constructor( getUpdateContentAction(currentState, wallets, selectedWallet) } - Timber.i("Resolved action: $action") + TangemLogger.i("Resolved action: $action") return action } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index bf79ed8458..92136614c6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.child.wallet.model.intents import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.tokens.TokenItemStateConverter.ApySource import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -40,7 +41,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("TooManyFunctions") @@ -131,12 +131,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( override fun onTokenItemLongClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) { modelScope.launch(dispatchers.main) { val userWalletId = accountId.userWalletId - val userWallet = getUserWalletUseCase(userWalletId).getOrElse { - Timber.e( + val userWallet = getUserWalletUseCase(userWalletId).getOrElse { error -> + TangemLogger.e( """ Unable to get user wallet |- ID: $userWalletId - |- Exception: $it + |- Exception: $error """.trimIndent(), ) @@ -287,7 +287,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( txHash = txHash, currency = currency, ).fold( - ifLeft = { Timber.e(it.toString()) }, + ifLeft = { TangemLogger.e(it.toString()) }, ifRight = { router.openUrl(url = it) }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index c3b9d8d1f2..5d86ed9512 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.child.wallet.model.intents import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.common.TangemBlogUrlBuilder import com.tangem.common.routing.AppRoute.* import com.tangem.common.routing.AppRouter @@ -11,8 +12,8 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic.ButtonSupport import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.navigation.review.ReviewManager +import com.tangem.core.navigation.url.UrlOpener import com.tangem.domain.card.SetCardWasScannedUseCase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.feedback.GetWalletMetaInfoUseCase @@ -53,7 +54,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("TooManyFunctions") @@ -187,7 +187,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( userWalletId = userWallet.walletId, currencies = missedAddressCurrencies, ).fold( - ifLeft = { Timber.e(it, "Failed to derive public keys") }, + ifLeft = { TangemLogger.e("Failed to derive public keys", it) }, ifRight = { fetchCryptoCurrencies(userWalletId = userWallet.walletId, currencies = missedAddressCurrencies) }, @@ -485,7 +485,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( networks = currencies.map(CryptoCurrency::network).toSet(), ), ) - .onLeft { Timber.e("Unable to fetch networks: $it") } + .onLeft { TangemLogger.e("Unable to fetch networks: $it") } }, async { multiQuoteStatusFetcher( @@ -494,7 +494,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( appCurrencyId = null, ), ) - .onLeft { Timber.e("Unable to fetch quotes: $it") } + .onLeft { TangemLogger.e("Unable to fetch quotes: $it") } }, async { val stakingIds = currencies.mapNotNullTo(hashSetOf()) { @@ -507,7 +507,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( stakingIds = stakingIds, ), ) - .onLeft { Timber.e("Unable to fetch yield balances: $it") } + .onLeft { TangemLogger.e("Unable to fetch yield balances: $it") } }, ) .awaitAll() @@ -516,12 +516,12 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private fun getSelectedUserWallet(): UserWallet? { val userWalletId = stateHolder.getSelectedWalletId() - return getUserWalletUseCase(userWalletId).getOrElse { - Timber.e( + return getUserWalletUseCase(userWalletId).getOrElse { error -> + TangemLogger.e( """ Unable to get user wallet |- ID: $userWalletId - |- Exception: $it + |- Exception: $error """.trimIndent(), ) @@ -538,7 +538,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( setNotificationsEnabledUseCase(userWalletId, true).onRight { notificationsRepository.setNotificationsWasEnabledAutomatically(userWalletId.stringValue) }.onLeft { - Timber.e(it) + TangemLogger.e("Error", it) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt index e2c99b4aa3..0321bc421a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt @@ -36,7 +36,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import kotlin.time.Duration.Companion.seconds @Suppress("LongParameterList") @@ -72,12 +72,12 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( private fun findSelectedWallet(promoCode: String) { getSelectedWalletSyncUseCase().fold( - ifLeft = { - Timber.tag(LOG_TAG).e("Error on getting user wallet: $it") + ifLeft = { error -> + TangemLogger.withTag(LOG_TAG).e("Error on getting user wallet: $error") showAlert(Failed) }, ifRight = { userWallet -> - Timber.tag(LOG_TAG).d("SelectedUserWallet ${userWallet.walletId.stringValue.mask()}") + TangemLogger.withTag(LOG_TAG).d("SelectedUserWallet ${userWallet.walletId.stringValue.mask()}") findBitcoinAddress(userWallet = userWallet, promoCode = promoCode) }, ) @@ -96,24 +96,24 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( }, ) - Timber.tag(LOG_TAG).d("All user network statuses ${networkStatuses?.size}") + TangemLogger.withTag(LOG_TAG).d("All user network statuses ${networkStatuses?.size}") val cryptoCurrencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull( MultiWalletCryptoCurrenciesProducer.Params(userWallet.walletId), ) - Timber.tag(LOG_TAG).d("All user cryptoCurrencies on main ${cryptoCurrencies?.size}") + TangemLogger.withTag(LOG_TAG).d("All user cryptoCurrencies on main ${cryptoCurrencies?.size}") val bitcoinCurrency = cryptoCurrencies?.firstOrNull { it.id.rawNetworkId == Blockchain.Bitcoin.id } - Timber.tag(LOG_TAG).d("BitcoinCurrency $bitcoinCurrency") + TangemLogger.withTag(LOG_TAG).d("BitcoinCurrency $bitcoinCurrency") val bitcoinStatus = networkStatuses?.firstOrNull { status -> status.network.id == bitcoinCurrency?.network?.id } - Timber.tag(LOG_TAG).d("BitcoinStatus $bitcoinStatus") + TangemLogger.withTag(LOG_TAG).d("BitcoinStatus $bitcoinStatus") if (bitcoinStatus == null) { - Timber.tag(LOG_TAG).d("No bitcoin, bitcoin network status == null") + TangemLogger.withTag(LOG_TAG).d("No bitcoin, bitcoin network status == null") showAlert(NoBitcoinAddress) } else { val networkAddress = when (bitcoinStatus.value) { @@ -126,7 +126,7 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( ?.defaultAddress?.value if (bitcoinAddress != null) { - Timber.tag(LOG_TAG).d( + TangemLogger.withTag(LOG_TAG).d( "Start activation promoCode ${promoCode.mask()} address ${bitcoinAddress.mask()}", ) @@ -138,7 +138,7 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( } else { uiMessageSender.send(GlobalLoadingMessage(false)) delay(DEFAULT_MESSAGE_SENDER_DELAY) - Timber.tag(LOG_TAG).d("No Bitcoin address $bitcoinStatus.value") + TangemLogger.withTag(LOG_TAG).d("No Bitcoin address $bitcoinStatus.value") showAlert(NoBitcoinAddress) } } @@ -155,13 +155,15 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( delay(DEFAULT_MESSAGE_SENDER_DELAY) uiMessageSender.send(GlobalLoadingMessage(false)) delay(DEFAULT_MESSAGE_SENDER_DELAY) - Timber.tag(LOG_TAG).d("${promoCode.mask()} activation success on address ${bitcoinAddress.mask()}") + TangemLogger.withTag( + LOG_TAG, + ).d("${promoCode.mask()} activation success on address ${bitcoinAddress.mask()}") showAlert(Activated) }.onLeft { error -> delay(DEFAULT_MESSAGE_SENDER_DELAY) uiMessageSender.send(GlobalLoadingMessage(false)) delay(DEFAULT_MESSAGE_SENDER_DELAY) - Timber.tag(LOG_TAG).d("${promoCode.mask()} activation failed $error") + TangemLogger.withTag(LOG_TAG).d("${promoCode.mask()} activation failed $error") val alertType = when (error) { ActivatePromoCodeError.ActivationFailed -> Failed ActivatePromoCodeError.InvalidPromoCode -> InvalidPromoCode @@ -203,7 +205,7 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( @Suppress("NullableToStringCall") private fun saveAndBindRefcode() { scope.launch(dispatchers.default) { - Timber.i("saveAndBindRefcode: refcode = $refcode, campaign = $campaign") + TangemLogger.i("saveAndBindRefcode: refcode = $refcode, campaign = $campaign") if (!refcode.isNullOrBlank()) { val conversionData = AppsFlyerConversionData(refcode = refcode, campaign = campaign) @@ -211,7 +213,7 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( appsFlyerStore.storeIfAbsent(value = conversionData) bindRefcodeWithWalletUseCase(conversionData) - .onLeft { Timber.e("Failed to bind refcode with wallets: $it") } + .onLeft { TangemLogger.e("Failed to bind refcode with wallets: $it") } } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt index cb7eb1f324..4f32140696 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/HasSingleWalletSignedHashesUseCase.kt @@ -7,9 +7,9 @@ import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import timber.log.Timber import javax.inject.Inject @ModelScoped @@ -42,7 +42,7 @@ class HasSingleWalletSignedHashesUseCase @Inject constructor( }, ) } catch (e: IllegalArgumentException) { - Timber.w(e, "Unable to validate signature count: user wallet not found") + TangemLogger.w("Unable to validate signature count: user wallet not found", e) false } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt index 2af73a2491..a757334ff9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt @@ -18,7 +18,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.withContext -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @ModelScoped @@ -58,7 +58,7 @@ internal class OnrampStatusFactory @Inject constructor( if (!onrampTx.activeStatus.isTerminal) { getOnrampStatusUseCase(userWallet = userWallet, onrampTx.info.txId).fold( ifLeft = { - Timber.e("Couldn't update onramp status. $it") + TangemLogger.e("Couldn't update onramp status. $it") }, ifRight = { statusModel -> val txId = statusModel.txId diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt index 4b0d406ee5..53d32e9062 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/UseCaseExt.kt @@ -1,17 +1,17 @@ package com.tangem.feature.wallet.presentation.wallet.domain +import com.tangem.utils.logging.TangemLogger import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import timber.log.Timber internal fun GetSelectedWalletSyncUseCase.unwrap(): UserWallet? { return this().fold( - ifLeft = { - Timber.e("Impossible to get selected wallet $it") + ifLeft = { error -> + TangemLogger.e("Impossible to get selected wallet $error") null }, ifRight = { it }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt index 10de4aeeb6..b4cae53df1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import javax.inject.Singleton @@ -45,7 +45,7 @@ internal class WalletContentFetcher @Inject constructor( * (doesn't matter if it is active or not), then skip the update process. */ if (!forceUpdate && savedJobHolder != null && !savedJobHolder.isEmpty()) { - Timber.d("Skip fetching for $userWalletId") + TangemLogger.d("Skip fetching for $userWalletId") return@withContext } @@ -55,7 +55,7 @@ internal class WalletContentFetcher @Inject constructor( * then cancel the previous update. */ if (forceUpdate && savedJobHolder?.isActive == true) { - Timber.d("Cancel old fetching for $userWalletId") + TangemLogger.d("Cancel old fetching for $userWalletId") savedJobHolder.cancel() } @@ -63,7 +63,7 @@ internal class WalletContentFetcher @Inject constructor( JobHolder().also { fetchingJobMap[userWalletId] = it } } - Timber.d("Start fetching for $userWalletId") + TangemLogger.d("Start fetching for $userWalletId") val maybeResult = launch { walletBalanceFetcher( @@ -71,11 +71,11 @@ internal class WalletContentFetcher @Inject constructor( userWalletId = userWalletId, isPaymentAccountRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), - ).onLeft(Timber::e) + ).onLeft { TangemLogger.e("Error", it) } } .saveInAndJoin(jobHolder) - Timber.d("Finish fetching with result $maybeResult for $userWalletId") + TangemLogger.d("Finish fetching with result $maybeResult for $userWalletId") } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt index c6754b834f..7ead228271 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletNameMigrationUseCase.kt @@ -3,7 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.copy import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger class WalletNameMigrationUseCase( private val userWalletsListRepository: UserWalletsListRepository, @@ -17,13 +17,13 @@ class WalletNameMigrationUseCase( val wallets = userWalletsListRepository.userWalletsSync() val existingNames: MutableSet = mutableSetOf() - wallets.forEach { - val defaultName = it.name + wallets.forEach { wallet -> + val defaultName = wallet.name val suggestedWalletName = suggestedWalletName(defaultName, existingNames) if (defaultName != suggestedWalletName) { - userWalletsListRepository.saveWithoutLock(it.copy(name = suggestedWalletName), canOverride = true) + userWalletsListRepository.saveWithoutLock(wallet.copy(name = suggestedWalletName), canOverride = true) } - Timber.tag("Migrated names").e(it.walletId.toString() + " " + suggestedWalletName) + TangemLogger.withTag("Migrated names").e(wallet.walletId.toString() + " " + suggestedWalletName) } walletNamesMigrationRepository.setMigrationDone() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt index 8965195057..3eb88195ed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt @@ -7,7 +7,7 @@ import com.tangem.domain.models.wallet.isLocked import kotlinx.coroutines.CloseableCoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.newSingleThreadContext -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -45,19 +45,19 @@ internal class WalletScreenContentLoader @Inject constructor( storage.remove(id) loadInternal(userWallet, coroutineScope, isRefresh = true) } else { - Timber.d("$id content loading has already started") + TangemLogger.d("$id content loading has already started") } } } /** Cancel loading by [id] */ fun cancel(id: UserWalletId) { - Timber.d("$id content loading is canceled") + TangemLogger.d("$id content loading is canceled") storage.remove(id) } fun cancelAll() { - Timber.d("All content loading is canceled") + TangemLogger.d("All content loading is canceled") storage.clear() singleBackgroundDispatcher.close() } @@ -69,11 +69,11 @@ internal class WalletScreenContentLoader @Inject constructor( ) if (loader == null) { - Timber.e("Impossible to create loader for $userWallet") + TangemLogger.e("Impossible to create loader for $userWallet") return } - Timber.d("${userWallet.walletId} content loading is ${if (isRefresh) "re" else ""}started") + TangemLogger.d("${userWallet.walletId} content loading is ${if (isRefresh) "re" else ""}started") loader.subscribers .map { it.subscribe(coroutineScope, singleBackgroundDispatcher) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index 89bff8d53b..092521153e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -13,7 +13,7 @@ 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 com.tangem.utils.logging.TangemLogger import javax.inject.Inject import javax.inject.Singleton @@ -41,7 +41,7 @@ internal class WalletStateController @Inject constructor( } fun update(transformer: WalletScreenStateTransformer) { - Timber.d("Applying: ${transformer::class.simpleName}") + TangemLogger.d("Applying: ${transformer::class.simpleName}") mutableUiState.update(function = transformer::transform) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt index f30c88927b..1b72b3307d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt @@ -5,7 +5,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenSta import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class DeleteWalletTransformer( private val selectedWalletIndex: Int, @@ -31,7 +31,7 @@ internal class DeleteWalletTransformer( wallets = (prevState.wallets - deletedWalletState).toImmutableList(), ) else -> { - Timber.e("Wallets does not contain deleted wallet") + TangemLogger.e("Wallets does not contain deleted wallet") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RenameWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RenameWalletsTransformer.kt index af3f38b3a4..01563ac966 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RenameWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RenameWalletsTransformer.kt @@ -6,7 +6,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Transformer that renames wallets @@ -51,7 +51,7 @@ internal class RenameWalletsTransformer( is WalletState.MultiCurrency.Locked, is WalletState.SingleCurrency.Locked, -> { - Timber.e("Impossible to rename wallet in locked state") + TangemLogger.e("Impossible to rename wallet in locked state") prevState } } @@ -63,7 +63,7 @@ internal class RenameWalletsTransformer( prevState.copy(walletsBalanceUM = prevState.walletsBalanceUM.copySealed(name = newName)) } is WalletUM.Locked -> { - Timber.e("Impossible to rename wallet in locked state") + TangemLogger.e("Impossible to rename wallet in locked state") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt index f3dbd5dd9e..977e103f90 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt @@ -11,7 +11,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class SetCryptoCurrencyActionsTransformer( private val tokenActionsState: TokenActionsState, @@ -26,11 +26,11 @@ internal class SetCryptoCurrencyActionsTransformer( prevState.copy(buttons = tokenActionsState.toManageButtons()) } is WalletState.SingleCurrency.Locked -> { - Timber.w("Impossible to load primary currency status for locked wallet") + TangemLogger.w("Impossible to load primary currency status for locked wallet") prevState } is WalletState.MultiCurrency -> { - Timber.w("Impossible to load crypto currency actions for multi-currency wallet") + TangemLogger.w("Impossible to load crypto currency actions for multi-currency wallet") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt index 9d0afb1eff..510be12367 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt @@ -13,7 +13,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletOnrampTransactionConverter import kotlinx.collections.immutable.toPersistentList -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class SetExpressStatusesTransformer( userWalletId: UserWalletId, @@ -47,11 +47,11 @@ internal class SetExpressStatusesTransformer( ) } is WalletState.SingleCurrency.Locked -> { - Timber.w("Impossible to load express statuses for locked wallet") + TangemLogger.w("Impossible to load express statuses for locked wallet") prevState } is WalletState.MultiCurrency -> { - Timber.w("Impossible to load express statuses for multi-currency wallet") + TangemLogger.w("Impossible to load express statuses for multi-currency wallet") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt index 5efac70c6d..207eb330f0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt @@ -9,7 +9,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletMarketPriceConverter -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") internal class SetPrimaryCurrencyTransformer( @@ -27,11 +27,11 @@ internal class SetPrimaryCurrencyTransformer( ) } is WalletState.SingleCurrency.Locked -> { - Timber.w("Impossible to load primary currency status for locked wallet") + TangemLogger.w("Impossible to load primary currency status for locked wallet") prevState } is WalletState.MultiCurrency -> { - Timber.w("Impossible to load primary currency status for multi-currency wallet") + TangemLogger.w("Impossible to load primary currency status for multi-currency wallet") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index d2740b30c4..6bfbef7ab9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -13,7 +13,7 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.utils.disableButtons -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal internal class SetTokenListErrorTransformer( @@ -39,11 +39,11 @@ internal class SetTokenListErrorTransformer( ) } is WalletState.MultiCurrency.Locked -> { - Timber.w("Impossible to load tokens list for locked wallet") + TangemLogger.w("Impossible to load tokens list for locked wallet") prevState } is WalletState.SingleCurrency -> { - Timber.w("Impossible to load tokens list for single-currency wallet") + TangemLogger.w("Impossible to load tokens list for single-currency wallet") prevState } } @@ -68,7 +68,7 @@ internal class SetTokenListErrorTransformer( ) } is WalletUM.Locked -> { - Timber.w("Impossible to load tokens list for locked wallet") + TangemLogger.w("Impossible to load tokens list for locked wallet") walletUM } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 8bbaf12cb9..4d5d813ddc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -11,7 +11,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.converte import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.WalletTokensListUMConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal internal class SetTokenListTransformer( @@ -35,12 +35,12 @@ internal class SetTokenListTransformer( ) } is WalletState.MultiCurrency.Locked -> { - Timber.w("Impossible to load tokens list for locked wallet") + TangemLogger.w("Impossible to load tokens list for locked wallet") prevState } is WalletState.SingleCurrency, -> { - Timber.w("Impossible to load tokens list for single-currency wallet") + TangemLogger.w("Impossible to load tokens list for single-currency wallet") prevState } } @@ -56,7 +56,7 @@ internal class SetTokenListTransformer( ) } is WalletUM.Locked -> { - Timber.w("Impossible to load tokens list for locked wallet") + TangemLogger.w("Impossible to load tokens list for locked wallet") walletUM } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt index 9a2b1164f9..2773d8a536 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt @@ -12,7 +12,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemStateConverter import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class SetTxHistoryCountErrorTransformer( private val userWallet: UserWallet, @@ -46,7 +46,7 @@ internal class SetTxHistoryCountErrorTransformer( is WalletState.SingleCurrency.Locked, is WalletState.MultiCurrency, -> { - Timber.w("Impossible to load transactions history for multi-currency wallet") + TangemLogger.w("Impossible to load transactions history for multi-currency wallet") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt index cba6ac874b..bad52e78a8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt @@ -9,7 +9,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class SetTxHistoryCountTransformer( userWalletId: UserWalletId, @@ -24,11 +24,11 @@ internal class SetTxHistoryCountTransformer( ) is WalletState.SingleCurrency.Locked, -> { - Timber.w("Impossible to load transactions history for locked wallet") + TangemLogger.w("Impossible to load transactions history for locked wallet") prevState } is WalletState.MultiCurrency -> { - Timber.w("Impossible to load transactions history for multi-currency wallet") + TangemLogger.w("Impossible to load transactions history for multi-currency wallet") prevState } } @@ -40,7 +40,7 @@ internal class SetTxHistoryCountTransformer( private fun TxHistoryState.toLoadingState(): TxHistoryState { return if (this is TxHistoryState.Content) { - Timber.d("Load transactions history: $transactionsCount") + TangemLogger.d("Load transactions history: $transactionsCount") copy( contentItems = contentItems.apply { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt index b8af717c52..de0657e030 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt @@ -6,7 +6,7 @@ import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class SetTxHistoryItemsErrorTransformer( userWalletId: UserWalletId, @@ -18,11 +18,11 @@ internal class SetTxHistoryItemsErrorTransformer( return when (prevState) { is WalletState.SingleCurrency.Content -> prevState.copy(txHistoryState = createErrorState()) is WalletState.SingleCurrency.Locked -> { - Timber.w("Impossible to load transactions history for locked wallet") + TangemLogger.w("Impossible to load transactions history for locked wallet") prevState } is WalletState.MultiCurrency -> { - Timber.w("Impossible to load transactions history for multi-currency wallet") + TangemLogger.w("Impossible to load transactions history for multi-currency wallet") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt index 289daa4514..f13ad18498 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt @@ -9,7 +9,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemFlowConverter import kotlinx.coroutines.flow.Flow -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class SetTxHistoryItemsTransformer( userWallet: UserWallet, @@ -23,11 +23,11 @@ internal class SetTxHistoryItemsTransformer( txHistoryState = prevState.txHistoryState.toContentState(), ) is WalletState.SingleCurrency.Locked -> { - Timber.w("Impossible to load transactions history for locked wallet") + TangemLogger.w("Impossible to load transactions history for locked wallet") prevState } is WalletState.MultiCurrency -> { - Timber.w("Impossible to load transactions history for multi-currency wallet") + TangemLogger.w("Impossible to load transactions history for multi-currency wallet") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt index fde5efd497..9f36ea4e38 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt @@ -7,7 +7,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class SetWarningsTransformer( userWalletId: UserWalletId, @@ -23,7 +23,7 @@ internal class SetWarningsTransformer( is WalletState.MultiCurrency.Locked, is WalletState.SingleCurrency.Locked, -> { - Timber.w("Impossible to update notifications for locked wallet") + TangemLogger.w("Impossible to update notifications for locked wallet") prevState } } @@ -36,7 +36,7 @@ internal class SetWarningsTransformer( notificationsCarousel = notificationsCarousel, ) is WalletUM.Locked -> { - Timber.w("Impossible to update notifications for locked wallet") + TangemLogger.w("Impossible to update notifications for locked wallet") walletUM } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt index fcef9841df..2c1c7e542f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt @@ -11,7 +11,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class UnlockWalletTransformer( private val unlockedWallets: List, @@ -65,7 +65,7 @@ internal class UnlockWalletTransformer( is WalletState.MultiCurrency.Content, is WalletState.SingleCurrency.Content, -> { - Timber.e("Impossible to unlock wallet with not locked state") + TangemLogger.e("Impossible to unlock wallet with not locked state") prevState } } @@ -77,7 +77,7 @@ internal class UnlockWalletTransformer( userWallet = unlockedWallet, ) is WalletUM.Content -> { - Timber.e("Impossible to unlock wallet with not locked state") + TangemLogger.e("Impossible to unlock wallet with not locked state") walletUM } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt index 72c403ae19..2b4dd9e457 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt @@ -7,7 +7,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger internal class UpdateWalletCardsCountTransformer( private val userWallet: UserWallet, @@ -25,7 +25,7 @@ internal class UpdateWalletCardsCountTransformer( is WalletState.MultiCurrency.Locked, is WalletState.SingleCurrency.Locked, -> { - Timber.e("Impossible to update wallet cards count for locked wallet") + TangemLogger.e("Impossible to update wallet cards count for locked wallet") prevState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index 48aa789257..463dd73e6e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -19,7 +19,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetToken import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal /** @@ -126,7 +126,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { ?: return }, ifError = { e -> - Timber.e("Failed to load token list: $e") + TangemLogger.e("Failed to load token list: $e") stateController.update( SetTokenListErrorTransformer( selectedWallet = userWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt index 04f33d8338..cc818aa976 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt @@ -21,7 +21,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger @Suppress("LongParameterList") internal class TangemPayMainSubscriber @AssistedInject constructor( @@ -67,7 +67,7 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( } TangemPayCustomerInfoError.UnknownError -> { // hide TangemPay block - Timber.e("Failed when loading main screen TangemPay info: $tangemPayError") + TangemLogger.e("Failed when loading main screen TangemPay info: $tangemPayError") stateController.update( transformer = TangemPayHiddenStateTransformer(userWalletId), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt index 4b48ea2769..7afe494928 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletSubscriber.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Component for implementation of flow subscription @@ -18,7 +18,7 @@ internal abstract class WalletSubscriber { protected abstract fun create(coroutineScope: CoroutineScope): Flow<*> fun subscribe(coroutineScope: CoroutineScope, dispatchers: CoroutineDispatcher): Job { - Timber.d("Subscribe on ${this::class.simpleName}") + TangemLogger.d("Subscribe on ${this::class.simpleName}") return create(coroutineScope) .flowOn(dispatchers) diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt index 28fd3f1aec..de88d92e2a 100644 --- a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt @@ -45,7 +45,6 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test -import timber.log.Timber @OptIn(ExperimentalCoroutinesApi::class) class DefaultPromoDeeplinkHandlerTest { @@ -83,7 +82,6 @@ class DefaultPromoDeeplinkHandlerTest { messages = mutableListOf() every { uiMessageSender.send(capture(messages)) } just runs - Timber.uprootAll() } @Test diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index 48f84373fe..3c857c76b5 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(projects.core.decompose) implementation(projects.core.navigation) implementation(projects.core.ui) + implementation(projects.core.utils) /** Domain models */ implementation(projects.domain.account) @@ -73,7 +74,6 @@ dependencies { implementation(deps.arrow.core) implementation(deps.decompose.ext.compose) implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(tangemDeps.card.core) implementation(tangemDeps.blockchain) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt index 0bb89d47c0..bef2fd2b82 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt @@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Stable @@ -60,7 +60,7 @@ internal class WcConnectedAppInfoModel @Inject constructor( modelScope.launch { val session = wcSessionsUseCase.findByTopic(topic = topic) if (session == null) { - Timber.e("Can not find WcSession by topic: $topic") + TangemLogger.e("Can not find WcSession by topic: $topic") messageSender.send(SnackbarMessage(stringReference("Can not find WcSession by topic: $topic"))) dismiss() } else { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt index 56a76748db..916d18477d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt @@ -9,7 +9,7 @@ import com.tangem.features.walletconnect.connections.entity.* import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.ui.sign.accountPortfolioName import kotlinx.collections.immutable.persistentListOf -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.util.UUID internal object WcConnectionsPreviewData { @@ -24,13 +24,13 @@ internal object WcConnectionsPreviewData { iconUrl = "$BASE_URL/satoshi.png", subtitle = "https://react-app.walletconnect.com/", verifiedState = VerifiedDAppState.Verified {}, - onClick = { Timber.d("Moralis clicked") }, + onClick = { TangemLogger.d("Moralis clicked") }, ), WcConnectedAppInfo( name = "website.com", iconUrl = "$BASE_URL/doge_again_lol.jpg", subtitle = "https://react-app.walletconnect.com/", - onClick = { Timber.d("website.com clicked") }, + onClick = { TangemLogger.d("website.com clicked") }, verifiedState = VerifiedDAppState.Unknown, ), ), @@ -44,7 +44,7 @@ internal object WcConnectionsPreviewData { iconUrl = "$BASE_URL/LiteCoin.png", subtitle = "https://react-app.walletconnect.com/", verifiedState = VerifiedDAppState.Verified {}, - onClick = { Timber.d("React app clicked") }, + onClick = { TangemLogger.d("React app clicked") }, ), ), ), @@ -57,14 +57,14 @@ internal object WcConnectionsPreviewData { iconUrl = "$BASE_URL/satoshi.png", subtitle = "https://react-app.walletconnect.com/", verifiedState = VerifiedDAppState.Verified {}, - onClick = { Timber.d("Moralis clicked") }, + onClick = { TangemLogger.d("Moralis clicked") }, ), WcConnectedAppInfo( name = "website.com", iconUrl = "$BASE_URL/doge_again_lol.jpg", subtitle = "https://react-app.walletconnect.com/", verifiedState = VerifiedDAppState.Unknown, - onClick = { Timber.d("website.com clicked") }, + onClick = { TangemLogger.d("website.com clicked") }, ), ), ), @@ -76,7 +76,7 @@ internal object WcConnectionsPreviewData { name = "React app", iconUrl = "$BASE_URL/LiteCoin.png", subtitle = "https://react-app.walletconnect.com/", - onClick = { Timber.d("React app clicked") }, + onClick = { TangemLogger.d("React app clicked") }, verifiedState = VerifiedDAppState.Verified {}, ), ), @@ -90,13 +90,13 @@ internal object WcConnectionsPreviewData { iconUrl = "$BASE_URL/satoshi.png", subtitle = "https://react-app.walletconnect.com/", verifiedState = VerifiedDAppState.Verified {}, - onClick = { Timber.d("Moralis clicked") }, + onClick = { TangemLogger.d("Moralis clicked") }, ), WcConnectedAppInfo( name = "website.com", iconUrl = "$BASE_URL/doge_again_lol.jpg", subtitle = "https://react-app.walletconnect.com/", - onClick = { Timber.d("website.com clicked") }, + onClick = { TangemLogger.d("website.com clicked") }, verifiedState = VerifiedDAppState.Unknown, ), ), @@ -110,7 +110,7 @@ internal object WcConnectionsPreviewData { iconUrl = "$BASE_URL/LiteCoin.png", subtitle = "https://react-app.walletconnect.com/", verifiedState = VerifiedDAppState.Verified {}, - onClick = { Timber.d("React app clicked") }, + onClick = { TangemLogger.d("React app clicked") }, ), ), ), @@ -137,7 +137,7 @@ internal object WcConnectionsPreviewData { iconUrl = "$BASE_URL/LiteCoin.png", subtitle = "https://react-app.walletconnect.com/", verifiedState = VerifiedDAppState.Verified {}, - onClick = { Timber.d("React app clicked") }, + onClick = { TangemLogger.d("React app clicked") }, ) val walletHeader diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/deeplink/DefaultWalletConnectDeepLinkHandler.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/deeplink/DefaultWalletConnectDeepLinkHandler.kt index 5d82c115da..bc3e83159d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/deeplink/DefaultWalletConnectDeepLinkHandler.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/deeplink/DefaultWalletConnectDeepLinkHandler.kt @@ -8,7 +8,7 @@ import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLi import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import java.net.URLDecoder internal class DefaultWalletConnectDeepLinkHandler @AssistedInject constructor( @@ -24,7 +24,7 @@ internal class DefaultWalletConnectDeepLinkHandler @AssistedInject constructor( // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet getSelectedWalletSyncUseCase().fold( ifLeft = { - Timber.e("Error on getting user wallet: $it") + TangemLogger.e("Error on getting user wallet: $it") }, ifRight = { wallet -> val decodedWcUri = URLDecoder.decode(wcUri, DEFAULT_CHARSET_NAME) @@ -38,7 +38,7 @@ internal class DefaultWalletConnectDeepLinkHandler @AssistedInject constructor( }, ) } catch (e: Exception) { - Timber.e(e) + TangemLogger.e("Error", e) } } } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionRequestBlockUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionRequestBlockUMConverter.kt index cc68b795ce..4cd6fec245 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionRequestBlockUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcTransactionRequestBlockUMConverter.kt @@ -1,5 +1,6 @@ package com.tangem.features.walletconnect.transaction.converter +import com.tangem.utils.logging.TangemLogger import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase @@ -8,7 +9,6 @@ import com.tangem.features.walletconnect.transaction.entity.common.WcTransaction import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoItemUM import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList -import timber.log.Timber import javax.inject.Inject internal class WcTransactionRequestBlockUMConverter @Inject constructor( @@ -42,7 +42,7 @@ internal class WcTransactionRequestBlockUMConverter @Inject constructor( try { addAll(transactionParamsConverter.convert(params)) } catch (exception: Exception) { - Timber.e(exception, "Error while parsing transaction params - %s", params) + TangemLogger.e("Error while parsing transaction params - $params", exception) } } } diff --git a/features/welcome/impl/build.gradle.kts b/features/welcome/impl/build.gradle.kts index 09713985c8..e21ec865c7 100644 --- a/features/welcome/impl/build.gradle.kts +++ b/features/welcome/impl/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.ui) implementation(projects.core.analytics) + implementation(projects.core.utils) implementation(projects.common.routing) implementation(projects.common.ui) @@ -57,7 +58,6 @@ dependencies { implementation(deps.decompose) implementation(deps.decompose.ext.compose) implementation(deps.kotlin.immutable.collections) - implementation(deps.timber) implementation(tangemDeps.card.core) implementation(tangemDeps.blockchain) implementation(tangemDeps.hot.core) diff --git a/features/yield-supply/impl/build.gradle.kts b/features/yield-supply/impl/build.gradle.kts index ada6f291fb..d71b9cf36a 100644 --- a/features/yield-supply/impl/build.gradle.kts +++ b/features/yield-supply/impl/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.analytics) implementation(projects.core.analytics.models) + implementation(projects.core.utils) /** Compose */ implementation(tangemDeps.vico.core) @@ -69,7 +70,6 @@ dependencies { /** Other */ implementation(deps.decompose) implementation(deps.decompose.ext.compose) - implementation(deps.timber) implementation(deps.kotlin.immutable.collections) /** DI */ diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt index 8ddd253ffb..92b9ebf190 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/active/model/YieldSupplyActiveModel.kt @@ -40,7 +40,7 @@ import com.tangem.utils.transformer.update import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -202,7 +202,7 @@ internal class YieldSupplyActiveModel @Inject constructor( } }, ifEmpty = { - Timber.w("No currency status found: ${cryptoCurrency.id}") + TangemLogger.w("No currency status found: ${cryptoCurrency.id}") }, ) } @@ -210,7 +210,7 @@ internal class YieldSupplyActiveModel @Inject constructor( .launchIn(modelScope) }, ifLeft = { error -> - Timber.w(error.toString()) + TangemLogger.w(error.toString()) return@launch }, ) @@ -232,7 +232,7 @@ internal class YieldSupplyActiveModel @Inject constructor( apy = TextReference.Str(DASH_SIGN), ) } - Timber.e("Error loading token status") + TangemLogger.e("Error loading token status") } } } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/model/YieldSupplyEntryModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/model/YieldSupplyEntryModel.kt index 5314191db2..649028c56e 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/model/YieldSupplyEntryModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/entry/model/YieldSupplyEntryModel.kt @@ -16,7 +16,7 @@ import com.tangem.features.yield.supply.api.entry.YieldSupplyEntryRoute import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @ModelScoped @@ -43,7 +43,7 @@ internal class YieldSupplyEntryModel @Inject constructor( singleAccountStatusListSupplier.getSyncOrNull(userWalletId) .getCryptoCurrencyStatus(currency = cryptoCurrency) .onNone { - Timber.e("Failed to get CryptoCurrencyStatus: ${cryptoCurrency.id}") + TangemLogger.e("Failed to get CryptoCurrencyStatus: ${cryptoCurrency.id}") withContext(dispatchers.mainImmediate) { router.pop() } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index ac026fed7c..70194d75cc 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.yield.supply.impl.main.model import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -35,7 +36,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlin.properties.Delegates @@ -100,8 +100,8 @@ internal class YieldSupplyModel @Inject constructor( userWallet = wallet subscribeOnCurrencyStatusUpdates() }, - ifLeft = { - Timber.w(it.toString()) + ifLeft = { error -> + TangemLogger.w(error.toString()) return@launch }, ) @@ -132,7 +132,7 @@ internal class YieldSupplyModel @Inject constructor( onCryptoCurrencyStatusUpdated(cryptoCurrencyStatus) }, ifEmpty = { - Timber.w("Unable to get crypto currency status: ${cryptoCurrency.id}") + TangemLogger.w("Unable to get crypto currency status: ${cryptoCurrency.id}") }, ) } @@ -149,8 +149,8 @@ internal class YieldSupplyModel @Inject constructor( onStartEarningClick = ::onStartEarningClick, ), ) - }.onLeft { - Timber.e(it) + }.onLeft { error -> + TangemLogger.e("Error", error) uiState.update { YieldSupplyUM.Initial } } } @@ -261,7 +261,7 @@ internal class YieldSupplyModel @Inject constructor( } computeAndApplyShowInfoIcon(cryptoCurrencyStatus) }.onLeft { t -> - Timber.e(t) + TangemLogger.e("Error", t) uiState.update { YieldSupplyUM.Content( title = resourceReference( diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt index 08cbe91674..a4b555063f 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.yield.supply.impl.subcomponents.approve.model import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -12,15 +13,18 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.isHotWallet -import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase @@ -37,14 +41,12 @@ import com.tangem.features.yield.supply.impl.subcomponents.approve.YieldSupplyAp import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSupplyNotificationsComponent import com.tangem.features.yield.supply.impl.subcomponents.notifications.YieldSupplyNotificationsUpdateTrigger import com.tangem.features.yield.supply.impl.subcomponents.notifications.entity.YieldSupplyNotificationData -import com.tangem.core.ui.extensions.TextReference import com.tangem.utils.TangemBlogUrlBuilder import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.transformer.update import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -141,7 +143,7 @@ internal class YieldSupplyApproveModel @Inject constructor( network = cryptoCurrency.network, ).fold( ifLeft = { error -> - Timber.e(error.toString()) + TangemLogger.e(error.toString()) uiState.update(YieldSupplyTransactionReadyTransformer) analyticsEventHandler.send( YieldSupplyAnalytics.EarnErrors( @@ -219,8 +221,8 @@ internal class YieldSupplyApproveModel @Inject constructor( contractAddress = cryptoCurrency.contractAddress, spenderAddress = contractAddress, amount = null, - ).getOrElse { - Timber.e(it) + ).getOrElse { error -> + TangemLogger.e("Error", error) return } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt index 0e7ff956e2..4824105018 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.yield.supply.impl.subcomponents.startearning.model import arrow.core.getOrElse +import com.tangem.utils.logging.TangemLogger import com.tangem.blockchain.common.TransactionSender import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -14,13 +15,13 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations.getCryptoCurrencyStatus import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isHotWallet -import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -44,7 +45,6 @@ import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject import kotlin.properties.Delegates @@ -231,7 +231,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( sendMode = TransactionSender.MultipleTransactionSendMode.DEFAULT, ).fold( ifLeft = { error -> - Timber.e(error.toString()) + TangemLogger.e(error.toString()) uiState.update(YieldSupplyTransactionReadyTransformer) analytics.send( YieldSupplyAnalytics.EarnErrors( @@ -323,8 +323,8 @@ internal class YieldSupplyStartEarningModel @Inject constructor( } getCurrenciesStatusUpdates() }, - ifLeft = { - Timber.w(it.toString()) + ifLeft = { error -> + TangemLogger.w(error.toString()) showAlertError() }, ) @@ -362,7 +362,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( ) }, ifEmpty = { - Timber.w("Unable to get crypto currency status: ${cryptoCurrency.id}") + TangemLogger.w("Unable to get crypto currency status: ${cryptoCurrency.id}") showAlertError() }, ) diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt index fbee3f5bb8..2d62921de0 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt @@ -46,7 +46,7 @@ import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList") @@ -153,7 +153,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor( network = cryptoCurrency.network, ).fold( ifLeft = { error -> - Timber.e(error.toString()) + TangemLogger.e(error.toString()) uiState.update(YieldSupplyTransactionReadyTransformer) analytics.send( YieldSupplyAnalytics.EarnErrors( diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 4cde3e5c09..32ce0df01b 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -77,7 +77,6 @@ rekotlin = "1.0.4" retrofit = "2.11.0" retrofitMoshiConverter = "2.9.0" spongycastleCryptoCore = "1.58.0.0" -timber = "4.7.1" kermit = "2.1.0" viewBindingDelegate = "1.5.9" xmlShimmer = "1.1.3" @@ -284,7 +283,6 @@ reKotlin = { module = "org.rekotlin:rekotlin", version.ref = "rekotlin" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } retrofit-response-type-keeper = { module = "com.squareup.retrofit2:response-type-keeper", version.ref = "retrofit" } retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofitMoshiConverter" } -timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } viewBindingDelegate = { module = "com.github.kirich1409:viewbindingpropertydelegate-noreflection", version.ref = "viewBindingDelegate" } xmlShimmer = { module = "com.github.skydoves:androidveil", version.ref = "xmlShimmer" } diff --git a/libs/blockchain-sdk/build.gradle.kts b/libs/blockchain-sdk/build.gradle.kts index 42a75d606f..8e9d2b66df 100644 --- a/libs/blockchain-sdk/build.gradle.kts +++ b/libs/blockchain-sdk/build.gradle.kts @@ -37,7 +37,6 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.moshi) implementation(deps.moshi.kotlin) - implementation(deps.timber) ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) // endregion diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt index 6c0fb03aca..2327fa9623 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -7,7 +7,7 @@ import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.common.datastorage.BlockchainDataStorage import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.blockchainsdk.providers.BlockchainProviderTypes -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -26,7 +26,7 @@ internal class WalletManagerFactoryCreator @Inject constructor( ) { fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory { - Timber.i("Create WalletManagerFactory") + TangemLogger.i("Create WalletManagerFactory") return WalletManagerFactory( config = config, diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt index b988b3fce1..ba05506e1a 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/converters/BlockchainProviderTypesConverter.kt @@ -6,7 +6,7 @@ import com.tangem.blockchainsdk.providers.BlockchainProviderTypes import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.utils.converter.TwoWayConverter -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Converts [BlockchainProvidersResponse] to [BlockchainProviderTypes] and vice versa @@ -23,7 +23,7 @@ internal object BlockchainProviderTypesConverter : val providerTypes = ProviderTypeConverter.convertList(input = blockchainProviders) providerTypes.forEach { - if (it == null) Timber.e("$blockchain provider type is not supported") + if (it == null) TangemLogger.e("$blockchain provider type is not supported") } blockchain to providerTypes.filterNotNull() diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt index 4c04ab93ee..86a63d3888 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseLoader.kt @@ -6,7 +6,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import javax.inject.Singleton @@ -53,7 +53,7 @@ internal class BlockchainProvidersResponseLoader @Inject constructor( ) }, onFailure = { throwable -> - Timber.e(throwable, "Failed to load blockchain provider types from backend") + TangemLogger.e("Failed to load blockchain provider types from backend", throwable) localResponse.ifEmpty { null } }, ) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt index 6e1a918463..575672207a 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/BlockchainProvidersResponseMerger.kt @@ -5,7 +5,7 @@ import com.tangem.blockchainsdk.BlockchainProvidersResponse import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.datasource.local.config.providers.models.ProviderModel -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject /** @@ -81,7 +81,7 @@ internal class BlockchainProvidersResponseMerger @Inject internal constructor( "Remote config does not contain some blockchains or providers information", ) - Timber.e(exception) + TangemLogger.e("Error", exception) analyticsExceptionHandler.sendException( ExceptionAnalyticsEvent( diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/DevBlockchainProvidersTypesManager.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/DevBlockchainProvidersTypesManager.kt index 44587e8923..349870c0c8 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/DevBlockchainProvidersTypesManager.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/DevBlockchainProvidersTypesManager.kt @@ -8,7 +8,7 @@ import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Implementation of [BlockchainProvidersTypesManager] in DEV environment @@ -36,7 +36,7 @@ internal class DevBlockchainProvidersTypesManager( val changed = changedBlockchainProvidersStore.data.firstOrNull().orEmpty() if (changed.isEmpty()) { - Timber.i("Initialize ChangedBlockchainProvidersStore") + TangemLogger.i("Initialize ChangedBlockchainProvidersStore") changedBlockchainProvidersStore.updateData { BlockchainProviderTypesConverter.convertBack(initial) } diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt index c535a0f61a..d1d0a03326 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/providers/ProdBlockchainProvidersTypesManager.kt @@ -2,7 +2,7 @@ package com.tangem.blockchainsdk.providers import com.tangem.blockchainsdk.converters.BlockchainProviderTypesConverter import kotlinx.coroutines.flow.Flow -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import javax.inject.Singleton @@ -26,11 +26,11 @@ internal class ProdBlockchainProvidersTypesManager @Inject constructor( val response = blockchainProvidersResponseLoader.load() if (response == null) { - Timber.e("Error loading BlockchainProviderTypes") + TangemLogger.e("Error loading BlockchainProviderTypes") return } - Timber.i("Update BlockchainProviderTypes") + TangemLogger.i("Update BlockchainProviderTypes") blockchainProviderTypesStore.store( value = BlockchainProviderTypesConverter.convert(response), diff --git a/libs/crypto/build.gradle.kts b/libs/crypto/build.gradle.kts index 4b9238c713..6f903222b3 100644 --- a/libs/crypto/build.gradle.kts +++ b/libs/crypto/build.gradle.kts @@ -28,7 +28,6 @@ dependencies { // region Other deps implementation(deps.kotlin.coroutines) - implementation(deps.timber) // endregion // region Test libraries diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt index 32e90c0d4b..026215cd7c 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt @@ -4,7 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.isUTXO import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.models.network.Network -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** * Utility class to recognize the account node in a derivation path based on the blockchain type. @@ -36,7 +36,7 @@ class AccountNodeRecognizer(private val blockchain: Blockchain) { .takeIf { nodesCount >= it } if (index == null) { - Timber.e("Cannot determine account node index for ${blockchain.fullName}: ${derivationPath.rawPath}") + TangemLogger.e("Cannot determine account node index for ${blockchain.fullName}: ${derivationPath.rawPath}") } return index @@ -63,7 +63,7 @@ class AccountNodeRecognizer(private val blockchain: Blockchain) { fun recognize(derivationPath: DerivationPath): Long? { return runCatching { if (!blockchain.isAccountsSupported()) { - Timber.e("Account derivation is not supported for blockchain: ${blockchain.fullName}") + TangemLogger.e("Account derivation is not supported for blockchain: ${blockchain.fullName}") return null } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/MutableDerivationPath.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/MutableDerivationPath.kt index fe1f193f9a..a1d01ad5e5 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/MutableDerivationPath.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/MutableDerivationPath.kt @@ -3,7 +3,7 @@ package com.tangem.lib.crypto.derivation import com.tangem.blockchain.common.Blockchain import com.tangem.crypto.hdWallet.DerivationNode import com.tangem.crypto.hdWallet.DerivationPath -import timber.log.Timber +import com.tangem.utils.logging.TangemLogger /** Extension function to convert a [DerivationPath] into a [MutableDerivationPath] */ fun DerivationPath.toMutable(): MutableDerivationPath = MutableDerivationPath(value = this) @@ -33,7 +33,7 @@ class MutableDerivationPath internal constructor(val value: DerivationPath) { is DerivationNode.NonHardened -> DerivationNode.NonHardened(value) } } else { - Timber.e("Account node not found in the derivation path: ${this@MutableDerivationPath.value}") + TangemLogger.e("Account node not found in the derivation path: ${this@MutableDerivationPath.value}") } return DerivationPath(path = mutableNodes).toMutable() diff --git a/libs/tangem-sdk-api/build.gradle.kts b/libs/tangem-sdk-api/build.gradle.kts index f780f554c2..bf167d3a08 100644 --- a/libs/tangem-sdk-api/build.gradle.kts +++ b/libs/tangem-sdk-api/build.gradle.kts @@ -29,7 +29,6 @@ dependencies { } /** Other libraries */ - implementation(deps.timber) /** DI */ implementation(deps.hilt.android) From b4ae0c154723da83a57b8a40835d4e580d916b38 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 23 Mar 2026 13:47:35 +0400 Subject: [PATCH 05/75] Updated on 2026-08-14 --- .claude/rules/codestyle/drawable-naming.md | 19 +++ .claude/rules/domain/core-components.md | 107 ++++++++++++ .claude/rules/domain/core-models.md | 142 +++++++++++++++ .claude/rules/git-rules.md | 22 +++ .claude/rules/tangem-sdk.md | 16 ++ CLAUDE.md | 114 +++++++++++++ ....kt => UserWalletsListRepositoryModule.kt} | 2 +- core/datasource/CLAUDE.md | 30 ++++ .../converter/BlockchainSDKConfigConverter.kt | 122 ------------- .../converter/EnvironmentConfigConverter.kt | 41 ----- .../models/EnvironmentConfigModel.kt | 140 --------------- .../models/EnvironmentConfigModels.kt | 5 + domain/core/CLAUDE.md | 77 +++++++++ .../wallets/legacy/UserWalletsListManager.kt | 161 ------------------ 14 files changed, 533 insertions(+), 465 deletions(-) create mode 100644 .claude/rules/codestyle/drawable-naming.md create mode 100644 .claude/rules/domain/core-components.md create mode 100644 .claude/rules/domain/core-models.md create mode 100644 .claude/rules/git-rules.md create mode 100644 .claude/rules/tangem-sdk.md create mode 100644 CLAUDE.md rename app/src/main/java/com/tangem/tap/domain/userWalletList/di/{UserWalletsListManagerModule.kt => UserWalletsListRepositoryModule.kt} (99%) create mode 100644 core/datasource/CLAUDE.md delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/config/environment/models/EnvironmentConfigModel.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/config/environment/models/EnvironmentConfigModels.kt create mode 100644 domain/core/CLAUDE.md delete mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt diff --git a/.claude/rules/codestyle/drawable-naming.md b/.claude/rules/codestyle/drawable-naming.md new file mode 100644 index 0000000000..e18b07bd2e --- /dev/null +++ b/.claude/rules/codestyle/drawable-naming.md @@ -0,0 +1,19 @@ +# Image Resources + +## Naming + +There are 3 types of icons: + +1. Black or single color icon (naming: `ic_name_24`, where number is size) +2. Icon with constant color, and tint could be applied (naming: `img_name_24`) +3. Large image with different colors and shapes (naming: `ill_name`) + +Examples: + +1. `ic_chevron_24` +2. `img_walletconnect_24` +3. `ill_bussiness` + +## Attention + +For complex vector images (named with `ill_name`), you should use `.png` resources, because when the project is compiled, all complex vectors are converted to large, heavy PNGs for different dimensions. \ No newline at end of file diff --git a/.claude/rules/domain/core-components.md b/.claude/rules/domain/core-components.md new file mode 100644 index 0000000000..9b9584fd4e --- /dev/null +++ b/.claude/rules/domain/core-components.md @@ -0,0 +1,107 @@ +# Domain Components + +Key domain mechanisms that orchestrate data flow: suppliers, fetchers, and use cases. + +## Retrieving Core Models + +### UserWallet + +#### UserWalletsListRepository + +**Location:** `domain/common` — `com.tangem.domain.common.wallets.UserWalletsListRepository` + +Repository for managing user wallets list. Provides `StateFlow?>` for the wallets list and `StateFlow` for the selected wallet. Supports loading, selecting, saving, locking/unlocking (biometric, access code), deleting, and reordering wallets. + +### Account / AccountList + +#### SingleAccountSupplier + +**Location:** `domain/account` — `com.tangem.domain.account.supplier.SingleAccountSupplier` + +Supplier that provides a single `Account` by `AccountId`. Has convenience methods `filterPaymentAccount` and `filterCryptoPortfolioAccount` to filter by account subtype. + +#### SingleAccountListSupplier + +**Location:** `domain/account` — `com.tangem.domain.account.supplier.SingleAccountListSupplier` + +Supplier that provides an `AccountList` for a specific user wallet by `UserWalletId`. + +#### MultiAccountListSupplier + +**Location:** `domain/account` — `com.tangem.domain.account.supplier.MultiAccountListSupplier` + +Supplier that provides a list of `AccountList`s for all user wallets. Extends `FlowCachingSupplier`. + +#### SingleAccountListFetcher + +**Location:** `domain/account` — `com.tangem.domain.account.fetcher.SingleAccountListFetcher` + +Fetcher that fetches a list of accounts for a single wallet by `UserWalletId`. Extends `FlowFetcher`. + +### AccountStatus / AccountStatusList + +#### SingleAccountStatusSupplier + +**Location:** `domain/account/status` — `com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier` + +Supplier that provides a single `AccountStatus` by account identifier. Extends `FlowCachingSupplier`. + +#### SingleAccountStatusListSupplier + +**Location:** `domain/account/status` — `com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier` + +Same as `SingleAccountListSupplier` but provides `AccountStatusList` (accounts with balances) for a specific user wallet. + +#### MultiAccountStatusListSupplier + +**Location:** `domain/account/status` — `com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier` + +Same as `MultiAccountListSupplier` but provides a list of `AccountStatusList`s for all user wallets. + +### Network / NetworkStatus + +#### SingleNetworkStatusSupplier + +**Location:** `domain/networks` — `com.tangem.domain.networks.single.SingleNetworkStatusSupplier` + +Supplier of `NetworkStatus` for a specific network and wallet. Extends `FlowCachingSupplier`. + +#### MultiNetworkStatusSupplier + +**Location:** `domain/networks` — `com.tangem.domain.networks.multi.MultiNetworkStatusSupplier` + +Supplier of all `NetworkStatus`es (as `Set`) for a selected wallet. Extends `FlowCachingSupplier`. + +#### SingleNetworkStatusFetcher + +**Location:** `domain/networks` — `com.tangem.domain.networks.single.SingleNetworkStatusFetcher` + +Fetcher of network status for a single `Network` by `UserWalletId`. Extends `FlowFetcher`. + +#### MultiNetworkStatusFetcher + +**Location:** `domain/networks` — `com.tangem.domain.networks.multi.MultiNetworkStatusFetcher` + +Fetcher of network statuses for a set of `Network`s for a multi-currency wallet by `UserWalletId`. Extends `FlowFetcher`. + +## Updating Balances + +### WalletBalanceFetcher + +**Location:** `domain/tokens` — `com.tangem.domain.tokens.wallet.WalletBalanceFetcher` + +Fetcher of wallet balances by `UserWalletId`. Selects the appropriate fetching strategy based on wallet type (multi-wallet, single wallet with tokens, single wallet). Delegates to `BalanceFetchingOperations` for shared fetching logic. + +### CryptoCurrencyBalanceFetcher + +**Location:** `domain/account/status` — `com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher` + +Fetches and refreshes balances for specific crypto currencies. Uses per-wallet mutexes to allow concurrent refreshes for different wallets while preventing concurrent refreshes for the same wallet. Delegates to `BalanceFetchingOperations`. + +## Managing Portfolio (User Tokens) + +### ManageCryptoCurrenciesUseCase + +**Location:** `domain/account/status` — `com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase` + +Use case for adding and removing crypto currencies in an account. \ No newline at end of file diff --git a/.claude/rules/domain/core-models.md b/.claude/rules/domain/core-models.md new file mode 100644 index 0000000000..bd7bf66aba --- /dev/null +++ b/.claude/rules/domain/core-models.md @@ -0,0 +1,142 @@ +# Domain Models + +Core business models used across the application. Models are defined in `domain/models/` and `domain/account/`. + +## StatusSource + +**Location:** `domain/models` — `com.tangem.domain.models.StatusSource` + +Enum representing data loading/refresh status. Used across all status models (NetworkStatus, QuoteStatus, YieldBalance, CryptoCurrencyStatus.Sources): +- `CACHE` — initial status, data loaded from cache +- `ACTUAL` — terminal status, data successfully fetched from server +- `ONLY_CACHE` — terminal status, data could not be refreshed (only cached data available) + +## CryptoCurrency + +**Location:** `domain/models` — `com.tangem.domain.models.currency.CryptoCurrency` + +Sealed class representing a cryptocurrency — either a native coin (`Coin`) or a token (`Token`). Used throughout the application: portfolio, token search, swaps, buy/sell, staking, etc. + +## CryptoCurrencyStatus + +**Location:** `domain/models` — `com.tangem.domain.models.currency.CryptoCurrencyStatus` + +Model representing a currency with its balance state. Primarily used to display user's coin balance in the portfolio. Wraps `CryptoCurrency` with a `Value` sealed interface: + +| Value subtype | Description | +|---|---| +| `Loading` | First-time fetch; once data is loaded, subsequent updates use cache via StatusSource, bypassing Loading | +| `Loaded` | Full data available | +| `Custom` | Custom token in portfolio; some data may be missing (e.g., no balance if backend has no quotes for it) | +| `NoQuote` | Balance known, no price data | +| `NoAccount` | Account not created (e.g., Solana reserve) | +| `Unreachable` | Network error | +| `NoAmount` | Coin is added to portfolio but no blockchain data available for it | +| `MissedDerivation` | Coin has no derivations — failed to obtain a blockchain network address | + +All Value subtypes carry `sources: Sources` tracking data freshness per dimension: `networkSource`, `quoteSource`, `stakingBalanceSource`, and aggregated `total`. + +## Network + +**Location:** `domain/models` — `com.tangem.domain.models.network.Network` + +Represents a blockchain network (e.g., Ethereum, Bitcoin). Contains network metadata: ID, name, currency symbol, derivation path, standard type (ERC20, TRC20, BEP20, etc.), and capabilities (token support, transaction extras, name resolving). + +## NetworkStatus + +**Location:** `domain/models` — `com.tangem.domain.models.network.NetworkStatus` + +Blockchain balances for all tokens of a network at a specific address. Only `Verified` and `NoAccount` are cached. + +| Value subtype | Description | +|---|---| +| `Verified` | Successful response from blockchain | +| `Unreachable` | Failed response from blockchain | +| `NoAccount` | Blockchain-specific status for chains that require a deposit to an address before it can be used | +| `MissedDerivation` | Derivation failed — no blockchain network address | + +## QuoteStatus + +**Location:** `domain/models` — `com.tangem.domain.models.quote.QuoteStatus` + +Exchange rate between the app's selected fiat currency and a coin's currency. + +## YieldBalance + +**Location:** `domain/models` — `com.tangem.domain.models.staking.YieldBalance` + +Staking yield balance for a specific `StakingID` (integrationId + address). + +## TotalFiatBalance + +**Location:** `domain/models` — `com.tangem.domain.models.TotalFiatBalance` + +Aggregate fiat balance across all tokens. Sealed interface with three states: `Loading`, `Failed`, `Loaded(amount, source)`. + +## TokenList + +**Location:** `domain/models` — `com.tangem.domain.models.tokenlist.TokenList` + +List of cryptocurrency tokens for display in portfolio. Sealed interface with subtypes: +- `GroupedByNetwork` — tokens grouped by `Network`, each group contains a list of `CryptoCurrencyStatus` +- `Ungrouped` — flat list of `CryptoCurrencyStatus` +- `Empty` — no tokens + +All subtypes carry `totalFiatBalance: TotalFiatBalance` and `sortedBy: TokensSortType`. + +## Account + +**Location:** `domain/models` — `com.tangem.domain.models.account.Account` + +Model representing a user account. Subtypes: +- **`Account.CryptoPortfolio`** — crypto portfolio with coins. All tokens in the account share the account's derivation (main account is an exception). Has a `DerivationIndex`: `0` for main account, `1..19` for secondary +- **`Account.Payment`** — account for Visa card integration + +## AccountStatus + +**Location:** `domain/models` — `com.tangem.domain.models.account.AccountStatus` + +Model representing an account with balances. Has a similar structure to `Account`: `CryptoPortfolio` and `Payment` subtypes. + +## AccountList + +**Location:** `domain/account` — `com.tangem.domain.account.models.AccountList` + +List of all accounts for a user wallet (`UserWallet`). + +Business rules (enforced by factory returning `Either`): +- Accounts list cannot be empty +- Exactly 1 main account +- Max 20 active accounts (`MAX_ACCOUNTS_COUNT`), max 1000 archived +- No duplicate AccountIds or custom AccountNames +- `totalAccounts >= activeAccounts` + +## AccountStatusList + +**Location:** `domain/account` — `com.tangem.domain.account.models.AccountStatusList` + +Same as `AccountList` but with balances (wraps `AccountStatus` instead of `Account`). + +## UserWallet + +**Location:** `domain/models` — `com.tangem.domain.models.wallet.UserWallet` + +Top-level model representing a user's wallet stored in the app. Subtypes: +- **`Cold`** — wallet backed by a physical Tangem card (NFC). Contains `ScanResponse`, card info, backup state +- **`Hot`** — software (hot) wallet without a physical card + +## Model Hierarchy + +``` +UserWallet + └─ AccountList / AccountStatusList + └─ Account.CryptoPortfolio / AccountStatus.CryptoPortfolio + ├─ CryptoCurrency (Coin | Token) + │ └─ CryptoCurrencyStatus (currency + Value state) + │ ├─ built from NetworkStatus (per network) + │ ├─ built from QuoteStatus (per rawCurrencyId) + │ └─ built from YieldBalance (per stakingId) + ├─ AccountId (SHA-256 hash) + ├─ DerivationIndex (0 = main) + └─ CryptoPortfolioIcon (icon + color) +``` \ No newline at end of file diff --git a/.claude/rules/git-rules.md b/.claude/rules/git-rules.md new file mode 100644 index 0000000000..fbfba63f29 --- /dev/null +++ b/.claude/rules/git-rules.md @@ -0,0 +1,22 @@ +# Git Rules + +## Branch Naming + +| Type | Format | Example | +|------|--------|---------| +| Feature | `feature/AND-xxx_short_description` | `feature/AND-13391_balance_fetcher` | +| Bugfix | `bugfix/AND-xxx_short_description` | `bugfix/AND-14000_fix_crash` | +| Pre-release | `x.x_pre_release` | `5.36_pre_release` | + +**Key branches:** +- `develop` — main integration branch, all feature/bugfix branches merge here +- `x.x_pre_release` — branched from `develop` on the last day of sprint for the upcoming release; receives regression bugfixes and additional release items +- `release` — merging into this branch triggers appTester build and production artifacts; PRs come from `x.x_pre_release` + +## Commit Messages + +Format: `AND-xxx Description` + +- Start with the Jira task number (AND-xxx) +- Followed by a space and a short description in English +- Example: `[REDACTED_TASK_KEY] Finalize CryptoCurrencyBalanceFetcher refactoring` \ No newline at end of file diff --git a/.claude/rules/tangem-sdk.md b/.claude/rules/tangem-sdk.md new file mode 100644 index 0000000000..daeb149428 --- /dev/null +++ b/.claude/rules/tangem-sdk.md @@ -0,0 +1,16 @@ +# Tangem SDK & Libraries + +## In-house SDKs (via `tangem_dependencies.toml`) + +- **Blockchain SDK** (`com.tangem:blockchain`) — multichain SDK for working with blockchains: creating/signing transactions, fetching balances, managing addresses. Wrapped in `libs/blockchain-sdk/` +- **Card SDK** (`com.tangem.tangem-sdk-kotlin:core`, `:android`) — SDK for interacting with physical Tangem cards via NFC: scanning, wallet creation, key derivation, passcode management, backup. Wrapped in `libs/tangem-sdk-api/` +- **Hot SDK** (`com.tangem.tangem-hot-sdk-kotlin:core`, `:android`) — SDK for hot (software) wallets +- **Vico** (`com.tangem.vico`) — forked charting library Vico, adapted for project needs + +## Wrapper Modules (`libs/`) + +- `libs/blockchain-sdk/` — wrapper around Blockchain SDK, provides domain-level abstractions for blockchain operations +- `libs/tangem-sdk-api/` — wrapper around Card SDK, exposes NFC card interaction API to the app +- `libs/crypto/` — cryptographic utilities: derivation, address handling, blockchain-specific helpers +- `libs/auth/` — API key provider interfaces for external services (Express, StakeKit) +- `libs/visa/` — Visa integration: smart contracts, limits, balances via Web3j \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000000..32252fee68 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,114 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build & Test Commands + +```bash +# Build debug APK (Google flavor) +./gradlew :app:assembleGoogleDebug + +# Run all unit tests (debug/googleDebug variants + JVM modules) +./gradlew unitTest + +# Run tests for a single module +./gradlew :features:wallet:impl:testDebugUnitTest # Android library module +./gradlew :app:testGoogleDebugUnitTest # App module +./gradlew :domain:tokens:test # Pure JVM module + +# Run a single test class +./gradlew :core:ui:testDebugUnitTest --tests "com.tangem.core.ui.format.BigDecimalCryptoFormatTest" + +# Detekt (static analysis) — runs automatically via convention plugin on applicable modules +./gradlew detekt detektMain + +# Build UI tests APKs (for Marathon) +./gradlew :app:assembleGoogleMocked :app:assembleGoogleMockedAndroidTest +# 1. :app:assembleGoogleMocked — main APK (mocked build) +# 2. :app:assembleGoogleMockedAndroidTest — test APK with instrumented tests +``` + +**Product flavors:** `google` and `huawei` (dimension: `service`). Default development flavor is `google`. + +**Build types:** `debug`, `mocked`, `internal`, `external`, `release`. + +## Architecture Overview + +### Module Layers + +The project is a heavily modularized Android app (~220 modules) organized in layers: + +- **`app/`** — Application entry point, Hilt setup, navigation root +- **`domain/`** — Business logic and models. Each domain area (e.g., `tokens`, `wallets`, `card`) has a `models` submodule for pure data types and a core module for use cases +- **`data/`** — Repository implementations and data sources, mirrors domain structure +- **`features/`** — UI features using **API/Impl split pattern**: `features:foo:api` defines the public contract, `features:foo:impl` contains the implementation. This enforces clean dependency boundaries +- **`core/`** — Cross-cutting concerns: `ui`, `analytics`, `datasource`, `decompose`, `navigation`, `res`, `utils`, `security`, `pagination` +- **`common/`** — Shared models, routing, UI components, test utilities +- **`libs/`** — SDK wrappers: `blockchain-sdk`, `tangem-sdk-api`, `crypto`, `auth`, `visa` + +### Component Architecture (Decompose) + +The app uses [Decompose](https://github.com/arkivanov/Decompose) for lifecycle-aware components. Every feature screen follows this structure: + +**API module** (`features/{name}/api/`): +- `{Name}Component` interface implementing `ComposableContentComponent` +- Inner `Params` data class for input parameters +- Inner `Factory` interface: `fun create(context: AppComponentContext, params: Params): {Name}Component` + +**Impl module** (`features/{name}/impl/`): +- `Default{Name}Component` with `@AssistedInject` constructor taking `@Assisted appComponentContext: AppComponentContext` and `@Assisted params` +- Delegates `AppComponentContext by appComponentContext` +- Creates model via `getOrCreateModel(params)` +- `@Composable Content(modifier)` collects model state via `collectAsStateWithLifecycle()` +- Inner `@AssistedFactory` interface extending the public `Factory` + +**Model** (`features/{name}/impl/.../model/`): +- `{Name}Model` extending `Model` base class, annotated `@ModelScoped`, uses `@Inject` constructor +- Receives params via `ParamsContainer.require()` +- Exposes `StateFlow<{Name}UM>` (UM = UI Model, state class in `ui/state/` subpackage) +- Has `modelScope` (SupervisorJob + mainImmediate), auto-cancelled on destroy + +**Child navigation within features:** +- `childStack()` — stacked screen navigation (back stack) +- `childSlot()` — optional overlays/bottom sheets (single or no child) +- `InnerRouter` — feature-internal navigation that delegates unknown routes to parent router + +### Feature Package Conventions + +- API package: `com.tangem.features.{name}.api` (plural `features`) +- Impl package: `com.tangem.feature.{name}.impl` (singular `feature` — legacy inconsistency, follow existing pattern per feature) +- Component: `{Name}Component` (api), `Default{Name}Component` (impl) +- Model: `{Name}Model` in `model/` subpackage +- UI State: `{Name}UM` in `ui/state/` subpackage +- UI Composable: in `ui/` subpackage + +### Key Frameworks & Patterns + +- **DI:** Hilt with `@SingletonComponent` scope and custom `@ModelScoped` scope for model-lifecycle dependencies +- **UI:** Jetpack Compose with Material3. Image loading via Coil +- **Navigation:** Custom `AppRouter` + `AppRoute` sealed classes with deep link support via `DeepLinkBuilder` +- **Networking:** Retrofit + Moshi for API communication +- **Local storage:** `AppPreferencesStore` for key-value pairs, `DataStore` for larger data +- **Async:** Kotlin Coroutines + Flow. Inject `CoroutineDispatcherProvider` (from `core/utils`) instead of using `Dispatchers.*` directly — provides `main`, `mainImmediate`, `io`, `default`, `single` +- **Error handling:** Arrow's `Either` pattern throughout domain/data layers. `DataError` sealed hierarchy for domain errors. See `domain/core/CLAUDE.md` for the LCE pattern +- **Analytics:** `AnalyticsEvent(category, event, params)` in `core/analytics/models/`. Feature events are sealed class hierarchies extending `AnalyticsEvent`. Send via injected `AnalyticsEventHandler` +- **Feature toggles:** `FeatureTogglesManager` in `core/config-toggles/`. Toggles are defined in `core/config-toggles/src/main/assets/configs/feature_toggles_config.json` and auto-generated into a `FeatureToggles` enum by the convention plugin at build time. Each feature module exposes its own `XxxFeatureToggles` interface (in `api/`) with a `DefaultXxxFeatureToggles` implementation (in `impl/`) that delegates to `FeatureTogglesManager` +- **Supported languages:** `SupportedLanguages` in `core/utils/` defines the app's supported locales: en, ru, de, fr, it, ja, uk, zh, es. `getCurrentSupportedLanguageCode()` returns the device locale if supported, otherwise falls back to English. Used by API calls that accept a language parameter + +### Build System + +- **Gradle 8.14.1**, AGP 8.10.1, Kotlin 2.1.10 +- **Version catalogs:** `gradle/dependencies.toml` (external/third-party dependencies) and `gradle/tangem_dependencies.toml` (in-house Tangem SDK dependencies) +- **Convention plugin:** `plugins/configuration/` — applies Detekt, configures test settings, generates environment configs and feature toggles +- **Custom Detekt rules:** `plugins/detekt-rules/`. Detekt configuration is in the `tangem-android-tools` git submodule. Key rule: `UnsafeStringResourceUsage` — prevents direct `stringResource()` / `pluralStringResource()` calls; use the `Safe`-suffixed variants instead +- **Localization:** Managed via [Lokalise](https://lokalise.com). Update strings by running `python3 lokalize.py` +- **GitHub Packages auth:** Requires `gpr.user` and `gpr.key` in `local.properties` for Tangem SDK dependencies + +### Testing + +- **JUnit 5** (Jupiter) for unit tests +- **MockK** for mocking +- **Turbine** for Flow testing +- **Truth** for assertions +- **Marathon** for UI tests (emulator-based, configured via `Marathonfile`) +- Shared test utilities in `common:test` and `test/core/` \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListRepositoryModule.kt similarity index 99% rename from app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt rename to app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListRepositoryModule.kt index 1fab5009e0..d7d9089767 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListRepositoryModule.kt @@ -41,7 +41,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object UserWalletsListManagerModule { +internal object UserWalletsListRepositoryModule { @Provides @Singleton diff --git a/core/datasource/CLAUDE.md b/core/datasource/CLAUDE.md new file mode 100644 index 0000000000..1d3332998b --- /dev/null +++ b/core/datasource/CLAUDE.md @@ -0,0 +1,30 @@ +# core/datasource + +## API Integration Guide + +### Config Structure + +- `ApiConfig` — base API config with `id` (`ApiConfig.ID`), `defaultEnvironment` (`ApiEnvironment`), and `environmentConfigs` (list of `ApiEnvironmentConfig`) +- `ApiEnvironmentConfig` — per-environment settings: `environment`, `baseUrl`, and `headers` (map of header name to `Provider`) + +### Config Management + +- `ApiConfigsManager` — DI-available component for accessing configs via `getEnvironmentConfig(id: ApiConfig.ID): ApiEnvironmentConfig` +- Two implementations: `ProdApiConfigsManager` (release) and `DevApiConfigsManager` (extends `MutableApiConfigsManager`, used when `BuildConfig.TESTER_MENU_ENABLED`) +- `MutableApiConfigsManager` allows runtime environment switching via Tester Menu without app restart + +### Adding a New API + +1. Create `ApiConfig` subclass in `com.tangem.datasource.api.common.config` — override `defaultEnvironment` and `environmentConfigs`. DI dependencies can be injected via constructor +2. Register the new config ID in `ApiConfig.initializeId(...)` +3. Provide the config in `ApiConfigsModule` using `@Provides @IntoSet` +4. Provide the API Retrofit service in `NetworkModule`: + - Get environment config: `apiConfigsManager.getEnvironmentConfig(id)` + - Use `environmentConfig.baseUrl` for Retrofit base URL + - Apply headers via `OkHttpClient.Builder().applyApiConfig(id, apiConfigsManager)` + +### Testing + +- Add the new config to `API_CONFIGS` list in `ProdApiConfigsManagerTest` +- Add a test model in the `data` method with expected `ApiEnvironmentConfig` values +- If the config has constructor dependencies, mock them and set up behavior in `setup()` \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt deleted file mode 100644 index fab336d555..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt +++ /dev/null @@ -1,122 +0,0 @@ -package com.tangem.datasource.local.config.environment.converter - -import com.tangem.blockchain.common.* -import com.tangem.datasource.local.config.environment.models.EnvironmentConfigModel -import com.tangem.utils.converter.Converter - -/** - * Converts [EnvironmentConfigModel] to [BlockchainSdkConfig] - * -[REDACTED_AUTHOR] - */ -internal object BlockchainSDKConfigConverter : Converter { - - override fun convert(value: EnvironmentConfigModel): BlockchainSdkConfig { - return BlockchainSdkConfig( - blockchairCredentials = BlockchairCredentials( - apiKey = value.blockchairApiKeys, - authToken = value.blockchairAuthorizationToken, - ), - blockcypherTokens = value.blockcypherTokens, - quickNodeSolanaCredentials = QuickNodeCredentials( - apiKey = value.quiknodeApiKey, - subdomain = value.quiknodeSubdomain, - ), - quickNodeBscCredentials = QuickNodeCredentials( - apiKey = value.bscQuiknodeApiKey, - subdomain = value.bscQuiknodeSubdomain, - ), - quickNodePlasmaCredentials = QuickNodeCredentials( - apiKey = value.quiknodePlasmaApiKey, - subdomain = value.quiknodePlasmaSubdomain, - ), - quickNodeMonadCredentials = QuickNodeCredentials( - apiKey = value.quiknodeMonadApiKey, - subdomain = value.quiknodeMonadSubdomain, - ), - infuraProjectId = value.infuraProjectId, - tronGridApiKey = value.tronGridApiKey, - nowNodeCredentials = NowNodeCredentials(value.nowNodesApiKey), - getBlockCredentials = createGetBlockCredentials(value), - kaspaSecondaryApiUrl = value.kaspaSecondaryApiUrl, - tonCenterCredentials = TonCenterCredentials( - mainnetApiKey = value.tonCenterKeys.mainnet, - testnetApiKey = value.tonCenterKeys.testnet, - ), - chiaFireAcademyApiKey = value.chiaFireAcademyApiKey, - chiaTangemApiKey = value.chiaTangemApiKey, - hederaArkhiaApiKey = value.hederaArkhiaKey, - polygonScanApiKey = value.polygonScanApiKey, - bittensorDwellirApiKey = value.bittensorDwellirApiKey, - bittensorOnfinalityApiKey = value.bittensorOnfinalityKey, - dwellirApiKey = value.dwellirApiKey, - koinosProApiKey = value.koinosProApiKey, - alephiumApiKey = value.alephiumTangemApiKey, - moralisApiKey = value.moralisApiKey, - etherscanApiKey = value.etherScanApiKey, - blinkApiKey = value.blinkApiKey, - tatumApiKey = value.tatumApiKey, - ) - } - - private fun createGetBlockCredentials(configValues: EnvironmentConfigModel): GetBlockCredentials? { - return configValues.getBlockAccessTokens?.let { accessTokens -> - GetBlockCredentials( - xrp = GetBlockAccessToken(jsonRpc = accessTokens.xrp?.jsonRPC), - cardano = GetBlockAccessToken(rosetta = accessTokens.cardano?.rosetta), - avalanche = GetBlockAccessToken(jsonRpc = accessTokens.avalanche?.jsonRPC), - eth = GetBlockAccessToken(jsonRpc = accessTokens.eth?.jsonRPC), - etc = GetBlockAccessToken(jsonRpc = accessTokens.etc?.jsonRPC), - fantom = GetBlockAccessToken(jsonRpc = accessTokens.fantom?.jsonRPC), - rsk = GetBlockAccessToken(jsonRpc = accessTokens.rsk?.jsonRPC), - bsc = GetBlockAccessToken(jsonRpc = accessTokens.bsc?.jsonRPC), - polygon = GetBlockAccessToken(jsonRpc = accessTokens.polygon?.jsonRPC), - gnosis = GetBlockAccessToken(jsonRpc = accessTokens.gnosis?.jsonRPC), - cronos = GetBlockAccessToken(jsonRpc = accessTokens.cronos?.jsonRPC), - solana = GetBlockAccessToken(jsonRpc = accessTokens.solana?.jsonRPC), - ton = GetBlockAccessToken(jsonRpc = accessTokens.ton?.jsonRPC), - tron = GetBlockAccessToken(rest = accessTokens.tron?.rest), - cosmos = GetBlockAccessToken(rest = accessTokens.cosmos?.rest), - near = GetBlockAccessToken(jsonRpc = accessTokens.near?.jsonRPC), - aptos = GetBlockAccessToken(rest = accessTokens.aptos?.rest), - dogecoin = GetBlockAccessToken( - jsonRpc = accessTokens.dogecoin?.jsonRPC, - blockBookRest = accessTokens.dogecoin?.blockBookRest, - ), - litecoin = GetBlockAccessToken( - jsonRpc = accessTokens.litecoin?.jsonRPC, - blockBookRest = accessTokens.litecoin?.blockBookRest, - ), - dash = GetBlockAccessToken( - jsonRpc = accessTokens.dash?.jsonRPC, - blockBookRest = accessTokens.dash?.blockBookRest, - ), - bitcoin = GetBlockAccessToken( - jsonRpc = accessTokens.bitcoin?.jsonRPC, - blockBookRest = accessTokens.bitcoin?.blockBookRest, - ), - algorand = GetBlockAccessToken(rest = accessTokens.algorand?.rest), - zkSyncEra = GetBlockAccessToken(jsonRpc = accessTokens.zksync?.jsonRPC), - polygonZkEvm = GetBlockAccessToken(jsonRpc = accessTokens.polygonZkevm?.jsonRPC), - base = GetBlockAccessToken(jsonRpc = accessTokens.base?.jsonRPC), - blast = GetBlockAccessToken(jsonRpc = accessTokens.blast?.jsonRPC), - filecoin = GetBlockAccessToken(jsonRpc = accessTokens.filecoin?.jsonRPC), - arbitrum = GetBlockAccessToken(jsonRpc = accessTokens.arbitrum?.jsonRPC), - bitcoinCash = GetBlockAccessToken( - jsonRpc = accessTokens.bitcoinCash?.jsonRPC, - blockBookRest = accessTokens.bitcoinCash?.blockBookRest, - ), - kusama = GetBlockAccessToken(jsonRpc = accessTokens.kusama?.jsonRPC), - moonbeam = GetBlockAccessToken(jsonRpc = accessTokens.moonbeam?.jsonRPC), - optimism = GetBlockAccessToken(jsonRpc = accessTokens.optimism?.jsonRPC), - polkadot = GetBlockAccessToken(jsonRpc = accessTokens.polkadot?.jsonRPC), - shibarium = GetBlockAccessToken(jsonRpc = accessTokens.shibarium?.jsonRPC), - sui = GetBlockAccessToken(jsonRpc = accessTokens.sui?.jsonRPC), - telos = GetBlockAccessToken(jsonRpc = accessTokens.telos?.jsonRPC), - tezos = GetBlockAccessToken(rest = accessTokens.tezos?.rest), - monad = GetBlockAccessToken(rest = accessTokens.monad?.rest), - stellar = GetBlockAccessToken(rest = accessTokens.stellar?.rest), - ) - } - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt deleted file mode 100644 index 6d7733109a..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/EnvironmentConfigConverter.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.datasource.local.config.environment.converter - -import com.tangem.datasource.local.config.environment.EnvironmentConfig -import com.tangem.datasource.local.config.environment.models.EnvironmentConfigModel -import com.tangem.utils.converter.Converter - -/** - * Converter from [EnvironmentConfigModel] to [EnvironmentConfig] - * -[REDACTED_AUTHOR] - */ -internal object EnvironmentConfigConverter : Converter { - - override fun convert(value: EnvironmentConfigModel): EnvironmentConfig { - return EnvironmentConfig( - moonPayApiKey = value.moonPayApiKey, - moonPayApiSecretKey = value.moonPayApiSecretKey, - mercuryoWidgetId = value.mercuryoWidgetId, - mercuryoSecret = value.mercuryoSecret, - blockchainSdkConfig = BlockchainSDKConfigConverter.convert(value = value), - amplitudeApiKey = value.amplitudeApiKey, - appsFlyerApiKey = value.appsFlyer.appsFlyerDevKey, - appsAppId = value.appsFlyer.appsFlyerAppID, - walletConnectProjectId = value.walletConnectProjectId, - express = value.express, - devExpress = value.devExpress, - stakeKitApiKey = value.stakeKitApiKey, - p2pApiKey = value.p2pApiKey, - blockAidApiKey = value.blockaidApiKey, - tangemApiKey = value.tangemApiKey, - tangemApiKeyDev = value.tangemApiKeyDev, - tangemApiKeyStage = value.tangemApiKeyStage, - yieldModuleApiKey = value.yieldModuleApiKey, - yieldModuleApiKeyDev = value.yieldModuleApiKeyDev, - bffStaticToken = value.bffStaticToken, - bffStaticTokenDev = value.bffStaticTokenDev, - gaslessTxApiKeyDev = value.gaslessTxApiKeyDev, - gaslessTxApiKey = value.gaslessTxApiKey, - ) - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/models/EnvironmentConfigModel.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/models/EnvironmentConfigModel.kt deleted file mode 100644 index 152c16e531..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/models/EnvironmentConfigModel.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.tangem.datasource.local.config.environment.models - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@Suppress("LongParameterList") -@JsonClass(generateAdapter = true) -class EnvironmentConfigModel( - @Json(name = "mercuryoWidgetId") val mercuryoWidgetId: String, - @Json(name = "mercuryoSecret") val mercuryoSecret: String, - @Json(name = "moonPayApiKey") val moonPayApiKey: String, - @Json(name = "moonPayApiSecretKey") val moonPayApiSecretKey: String, - @Json(name = "blockchairApiKeys") val blockchairApiKeys: List, - @Json(name = "blockchairAuthorizationToken") val blockchairAuthorizationToken: String?, - @Json(name = "quiknodeSubdomain") val quiknodeSubdomain: String, - @Json(name = "quiknodeApiKey") val quiknodeApiKey: String, - @Json(name = "bscQuiknodeSubdomain") val bscQuiknodeSubdomain: String, - @Json(name = "bscQuiknodeApiKey") val bscQuiknodeApiKey: String, - @Json(name = "quiknodePlasmaSubdomain") val quiknodePlasmaSubdomain: String, - @Json(name = "quiknodePlasmaApiKey") val quiknodePlasmaApiKey: String, - @Json(name = "quiknodeMonadSubdomain") val quiknodeMonadSubdomain: String, - @Json(name = "quiknodeMonadApiKey") val quiknodeMonadApiKey: String, - @Json(name = "nowNodesApiKey") val nowNodesApiKey: String, - @Json(name = "getBlockAccessTokens") val getBlockAccessTokens: GetBlockAccessTokens?, - @Json(name = "tonCenterApiKey") val tonCenterKeys: TonCenterKeys, - @Json(name = "blockcypherTokens") val blockcypherTokens: Set?, - @Json(name = "infuraProjectId") val infuraProjectId: String?, - @Json(name = "tronGridApiKey") val tronGridApiKey: String, - @Json(name = "amplitudeApiKey") val amplitudeApiKey: String, - @Json(name = "appsFlyer") val appsFlyer: AppsFlyerModel, - @Json(name = "kaspaSecondaryApiUrl") val kaspaSecondaryApiUrl: String, - @Json(name = "walletConnectProjectId") val walletConnectProjectId: String, - @Json(name = "chiaFireAcademyApiKey") val chiaFireAcademyApiKey: String?, - @Json(name = "chiaTangemApiKey") val chiaTangemApiKey: String?, - @Json(name = "devExpress") val devExpress: ExpressModel?, - @Json(name = "express") val express: ExpressModel?, - @Json(name = "hederaArkhiaKey") val hederaArkhiaKey: String?, - @Json(name = "polygonScanApiKey") val polygonScanApiKey: String?, - @Json(name = "stakeKitApiKey") val stakeKitApiKey: String?, - @Json(name = "p2pApiKey") val p2pApiKey: P2PKeys?, - @Json(name = "bittensorDwellirKey") val bittensorDwellirApiKey: String?, - @Json(name = "bittensorOnfinalityKey") val bittensorOnfinalityKey: String?, - @Json(name = "dwellirApiKey") val dwellirApiKey: String?, - @Json(name = "koinosProApiKey") val koinosProApiKey: String?, - @Json(name = "alephiumTangemApiKey") val alephiumTangemApiKey: String?, - @Json(name = "moralisApiKey") val moralisApiKey: String?, - @Json(name = "nftScanApiKey") val nftScanApiKey: String?, - @Json(name = "blockaidApiKey") val blockaidApiKey: String?, - @Json(name = "tangemApiKey") val tangemApiKey: String?, - @Json(name = "tangemApiKeyDev") val tangemApiKeyDev: String?, - @Json(name = "tangemApiKeyStage") val tangemApiKeyStage: String?, - @Json(name = "etherscanApiKey") val etherScanApiKey: String?, - @Json(name = "yieldModuleApiKey") val yieldModuleApiKey: String?, - @Json(name = "yieldModuleApiKeyDev") val yieldModuleApiKeyDev: String?, - @Json(name = "blinkApiKey") val blinkApiKey: String?, - @Json(name = "tatumApiKey") val tatumApiKey: String?, - @Json(name = "bffStaticToken") val bffStaticToken: String?, - @Json(name = "bffStaticTokenDev") val bffStaticTokenDev: String?, - @Json(name = "gaslessTxApiKeyDev") val gaslessTxApiKeyDev: String?, - @Json(name = "gaslessTxApiKey") val gaslessTxApiKey: String?, -) - -@JsonClass(generateAdapter = true) -data class GetBlockAccessTokens( - @Json(name = "xrp") val xrp: GetBlockToken?, - @Json(name = "cardano") val cardano: GetBlockToken?, - @Json(name = "avalanche") val avalanche: GetBlockToken?, - @Json(name = "ethereum") val eth: GetBlockToken?, - @Json(name = "ethereumClassic") val etc: GetBlockToken?, - @Json(name = "fantom") val fantom: GetBlockToken?, - @Json(name = "rsk") val rsk: GetBlockToken?, - @Json(name = "bsc") val bsc: GetBlockToken?, - @Json(name = "polygon") val polygon: GetBlockToken?, - @Json(name = "xdai") val gnosis: GetBlockToken?, - @Json(name = "cronos") val cronos: GetBlockToken?, - @Json(name = "solana") val solana: GetBlockToken?, - @Json(name = "ton") val ton: GetBlockToken?, - @Json(name = "tron") val tron: GetBlockToken?, - @Json(name = "cosmos-hub") val cosmos: GetBlockToken?, - @Json(name = "near") val near: GetBlockToken?, - @Json(name = "aptos") val aptos: GetBlockToken?, - @Json(name = "dogecoin") val dogecoin: GetBlockToken?, - @Json(name = "litecoin") val litecoin: GetBlockToken?, - @Json(name = "dash") val dash: GetBlockToken?, - @Json(name = "bitcoin") val bitcoin: GetBlockToken?, - @Json(name = "algorand") val algorand: GetBlockToken?, - @Json(name = "polygon-zkevm") val polygonZkevm: GetBlockToken?, - @Json(name = "zksync") val zksync: GetBlockToken?, - @Json(name = "base") val base: GetBlockToken?, - @Json(name = "blast") val blast: GetBlockToken?, - @Json(name = "filecoin") val filecoin: GetBlockToken?, - @Json(name = "arbitrum-one") val arbitrum: GetBlockToken?, - @Json(name = "bitcoinCash") val bitcoinCash: GetBlockToken?, - @Json(name = "kusama") val kusama: GetBlockToken?, - @Json(name = "moonbeam") val moonbeam: GetBlockToken?, - @Json(name = "optimism") val optimism: GetBlockToken?, - @Json(name = "polkadot") val polkadot: GetBlockToken?, - @Json(name = "shibarium") val shibarium: GetBlockToken?, - @Json(name = "sui") val sui: GetBlockToken?, - @Json(name = "telos") val telos: GetBlockToken?, - @Json(name = "tezos") val tezos: GetBlockToken?, - @Json(name = "monad") val monad: GetBlockToken?, - @Json(name = "stellar") val stellar: GetBlockToken?, -) - -@JsonClass(generateAdapter = true) -data class TonCenterKeys( - @Json(name = "mainnet") val mainnet: String, - @Json(name = "testnet") val testnet: String, -) - -@JsonClass(generateAdapter = true) -data class P2PKeys( - @Json(name = "mainnet") val mainnet: String, - @Json(name = "hoodi") val hoodi: String, -) - -@JsonClass(generateAdapter = true) -data class GetBlockToken( - @Json(name = "jsonRpc") val jsonRPC: String?, - @Json(name = "blockBookRest") val blockBookRest: String?, - @Json(name = "rest") val rest: String?, - @Json(name = "rosetta") val rosetta: String?, -) - -@JsonClass(generateAdapter = true) -data class ExpressModel( - @Json(name = "apiKey") - val apiKey: String, - @Json(name = "signVerifierPublicKey") - val signVerifierPublicKey: String, -) - -@JsonClass(generateAdapter = true) -data class AppsFlyerModel( - @Json(name = "appsFlyerDevKey") - val appsFlyerDevKey: String, - @Json(name = "appsFlyerAppID") - val appsFlyerAppID: String, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/models/EnvironmentConfigModels.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/models/EnvironmentConfigModels.kt new file mode 100644 index 0000000000..b3dcc73c77 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/models/EnvironmentConfigModels.kt @@ -0,0 +1,5 @@ +package com.tangem.datasource.local.config.environment.models + +data class ExpressModel(val apiKey: String, val signVerifierPublicKey: String) + +data class P2PKeys(val mainnet: String, val hoodi: String) \ No newline at end of file diff --git a/domain/core/CLAUDE.md b/domain/core/CLAUDE.md new file mode 100644 index 0000000000..facad07dc2 --- /dev/null +++ b/domain/core/CLAUDE.md @@ -0,0 +1,77 @@ +# domain/core + +Cross-cutting domain utilities for async data loading, error handling, and reactive streams. Not business logic — foundational abstractions used across all domain modules. + +## LCE (Loading-Content-Error) Pattern + +`Lce` — sealed class representing async operation state: +- `Loading(partialContent?)` — in progress, may carry partial data +- `Content(content)` — success +- `Error(error)` — failure with typed error + +Key APIs: +- `lce { }` builder — executes block in `LceRaise` context with Arrow's Raise DSL for typed error handling +- `lceFlow { }` builder — creates `LceFlow` (alias for `Flow>`) via channel-based producer DSL +- `LceRaise.bind()` — extracts content from Lce/Either or short-circuits on error +- Extensions: `fold()`, `map()`, `mapError()`, `toLce()`, `toEither()` + +## Flow Packaging + +A pattern for complex data streams where work on a single flow is split into three logically separate components: **Producer** (creation), **Supplier** (delivery/caching), and **Fetcher** (refresh). Use it only when you need flexibility in creating, reusing, fetching, and updating a data stream (e.g., network status). Do NOT use for simple cases like reading preferences. + +### FlowProducer + +`FlowProducer` — creates the data flow. Implement: +- `fallback: Data` — emitted when the flow throws an exception +- `produce(): Flow` — the actual flow creation logic + +Built-in `produceWithFallback()` catches errors, emits `fallback`, waits 2s, then retries — keeping the flow alive for subscribers. + +`FlowProducer.Factory` — creates a Producer from params. Typically implemented via Hilt `@AssistedFactory`. + +**Implementation pattern:** +1. Define interface extending `FlowProducer` with inner `Params` data class and `Factory` interface +2. Create `Default*Producer` with `@AssistedInject` constructor taking `@Assisted params` + dependencies +3. Override `fallback` and `produce()` +4. Declare inner `@AssistedFactory` interface extending the Producer's Factory + +### FlowSupplier / FlowCachingSupplier + +`FlowSupplier` — delivers a flow by params via `operator fun invoke(params): Flow`. Also provides `getSyncOrNull(params, timeout)` for one-shot access. + +`FlowCachingSupplier` — abstract implementation that caches flows by key. Implement: +- `factory: FlowProducer.Factory` — to create producers +- `keyCreator: (Params) -> String` — to generate cache keys + +Behavior: returns cached flow if exists, otherwise creates via `factory.create(params).produceWithFallback()`, caches it, and auto-evicts on terminal exception. + +**Implementation pattern:** +1. Define abstract class extending `FlowCachingSupplier` with `factory` and `keyCreator` in constructor +2. In DI module, create anonymous subclass providing the factory (injected) and keyCreator lambda + +### FlowFetcher + +`FlowFetcher` — triggers data refresh, returns `Either`. Typically updates a store/data source, causing the Producer's flow to re-emit. + +**Implementation pattern:** +1. Define interface extending `FlowFetcher` with inner `Params` data class +2. Create `Default*Fetcher` with `@Inject` constructor, override `invoke` wrapping logic in `Either.catch { }`, handle errors with `.onLeft { }` + +### Testing + +- **FlowProducer**: test flow creation logic, params usage, emission behavior, exception handling +- **FlowFetcher**: test successful update path and error path (exception thrown) + +## Chain Processing + +- `Chain` / `ResultChain` — single operation in a chain, works with `Either` +- `ChainProcessor` — folds chains sequentially, stops on first error + +## Error Types + +- `DataError` — sealed domain error hierarchy: `NetworkError.NoInternetConnection`, `UserWalletError.WrongUserWallet` + +## Either Extensions + +- `Either.catchOn(dispatcher, block)` — executes on dispatcher, catches exceptions +- `eitherOn(dispatcher, block)` — Raise DSL block on specified dispatcher \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt deleted file mode 100644 index cccba8a293..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManager.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.tangem.domain.wallets.legacy - -import com.tangem.common.CompletionResult -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.coroutines.flow.Flow - -interface UserWalletsListManager { - - /** - * Indicates that the [UserWalletsListManager] is [UserWalletsListManager.Lockable] - * */ - val isLockable: Boolean - - /** [Flow] with all saved [UserWallet]s updates */ - val userWallets: Flow> - - /** Count saved wallets updates */ - val savedWalletsCount: Flow - - /** [Flow] with selected [UserWallet] updates */ - @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") - val selectedUserWallet: Flow - - /** [List] with all saved [UserWallet]s updates */ - val userWalletsSync: List - - /** Selected [UserWallet] */ - @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") - val selectedUserWalletSync: UserWallet? - - /** Indicates that the [UserWalletsListManager] contains at least one saved [UserWallet] */ - val hasUserWallets: Boolean - - /** Count of saved user wallets */ - val walletsCount: Int - - /** - * Set [UserWallet] with provided [UserWalletId] as selected - * - * @param userWalletId [UserWalletId] of [UserWallet] which must be selected - * - * @return [CompletionResult.Success] with selected [UserWallet] or [CompletionResult.Failure] with - * [NoSuchElementException] if [UserWallet] with [userWalletId] not found - */ - suspend fun select(userWalletId: UserWalletId): CompletionResult - - /** - * Save provided user wallet and set it as selected - * - * @param userWallet [UserWallet] to save - * @param canOverride If false, then terminate with [UserWalletsListError.WalletAlreadySaved] when user tries - * to save an already saved card - * - * @return [CompletionResult] of operation - */ - suspend fun save(userWallet: UserWallet, canOverride: Boolean = false): CompletionResult - - /** - * Same as [save] but not change selected user wallet ID and not terminate with - * [UserWalletsListError.WalletAlreadySaved] if [UserWallet] already saved. - * Can terminate with [NoSuchElementException] if unable to find [UserWallet] with provided [UserWalletId]. - * - * @param userWalletId update [UserWallet] with that [UserWalletId] - * @param update lambda that receives stored [UserWallet] and returns updated [UserWallet] - * - * @return [CompletionResult.Success] with updated [UserWallet] or [CompletionResult.Failure] with - * [NoSuchElementException] if [UserWallet] with [userWalletId] not found - */ - suspend fun update( - userWalletId: UserWalletId, - update: suspend (UserWallet) -> UserWallet, - ): CompletionResult - - /** - * Delete saved [UserWallet]s with provided [UserWalletId]s. - * Sets [isLocked] as true if [userWallets] is empty or if all [userWallets] are locked. - * - * @param userWalletIds [UserWalletId]s of [UserWallet]s which must be deleted - * - * @return [CompletionResult] of operation - */ - suspend fun delete(userWalletIds: List): CompletionResult - - /** - * Clear all saved [UserWallet]s and set [isLocked] as true - * - * @return [CompletionResult] of operation - */ - suspend fun clear(): CompletionResult - - /** - * Get [UserWallet] with provided [UserWalletId] - * - * @return [CompletionResult.Success] with found [UserWallet] or [CompletionResult.Failure] with - * [NoSuchElementException] if [UserWallet] with [userWalletId] not found - */ - suspend fun get(userWalletId: UserWalletId): CompletionResult - - interface Lockable : UserWalletsListManager { - - /** - * Indicates that all [UserWallet]s is locked - * - * @see [isLocked] - * @see [UserWallet.isLocked] - */ - val lockedState: Flow - - /** - * Indicates that all [UserWallet]s is locked. Sync version. - * - * @see [lockedState] - * @see [UserWallet.isLocked] - */ - val isLocked: Boolean - - /** - * Receive saved [UserWallet]s, populate [userWallets] flow with it and set [lockedState] as false. - * - * @param type Defines the behavior of the operation. - * - * @return [CompletionResult] of operation, with selected [UserWallet] - * or null if there is no selected [UserWallet] - */ - suspend fun unlock(type: UnlockType): CompletionResult - - /** Remove [UserWallet]s from [userWallets] and set [lockedState] as true */ - fun lock() - - /** - * Defines the behavior of the [unlock] operation. - * */ - enum class UnlockType { - /** - * Ensures that all stored [UserWallet]s are unlocked, - * or throws [UserWalletsListError.NotAllUserWalletsUnlocked]. - * - * In this type [selectedUserWallet] is either a previously selected [UserWallet] or the first stored - * [UserWallet]. - * */ - ALL, - - /** - * Ensures that at least one stored [UserWallet] is unlocked, - * or throws [UserWalletsListError.NoUserWalletSelected]. - * - * In this type [selectedUserWallet] is the first stored and unlocked [UserWallet]. - * */ - ANY, - - /** - * Same as [ALL] type, but this type can not change [selectedUserWallet] while unlocking. - * */ - ALL_WITHOUT_SELECT, - } - } - - // For provider - companion object -} \ No newline at end of file From e43f438c1ac1afc50a1456cc8a69993619949c84 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 23 Mar 2026 19:32:54 +0700 Subject: [PATCH 06/75] Updated on 2026-08-14 --- .../ui/amountScreen/ui/AmountFieldV2.kt | 30 ++- .../ui/amountScreen/utils/AmountUtils.kt | 4 +- core/ui/src/main/res/drawable/ic_fixed.xml | 9 + core/ui/src/main/res/drawable/ic_floating.xml | 13 ++ .../data/swap/DefaultSwapRepositoryV2.kt | 20 +- .../domain/swap/models/SwapQuoteModel.kt | 2 +- .../v2/impl/amount/entity/SwapAmountUM.kt | 2 + .../amount/model/SwapAmountClickIntents.kt | 1 + .../v2/impl/amount/model/SwapAmountModel.kt | 139 ++++++++++--- .../impl/amount/model/SwapAmountQuoteUtils.kt | 10 +- .../converter/SwapAmountFieldConverter.kt | 97 +++++----- .../SwapAmountUpdateSubtitleConverter.kt | 63 ++++++ .../converter/SwapFromSubtitleConverter.kt | 69 +++++++ .../model/converter/SwapQuoteUMConverter.kt | 16 +- .../model/converter/SwapSubtitleResult.kt | 11 ++ .../converter/SwapToSubtitleConverter.kt | 73 +++++++ .../SwapAmountBalanceHiddenTransformer.kt | 77 +++----- .../SwapAmountErrorQuoteTransformer.kt | 58 ++++++ .../SwapAmountPrimaryReadyStateTransformer.kt | 5 +- ...wapAmountSecondaryReadyStateTransformer.kt | 28 ++- .../SwapAmountSelectQuoteTransformer.kt | 182 ++++++++++++++---- .../SwapAmountSetQuotesTransformer.kt | 125 ++++++++---- .../impl/amount/ui/SwapAmountBlockContent.kt | 2 +- .../v2/impl/amount/ui/SwapAmountContent.kt | 127 ++++++++---- .../ui/preview/SwapAmountClickIntentsStub.kt | 2 + .../ui/preview/SwapAmountContentPreview.kt | 13 +- .../SwapProviderListItemConverter.kt | 2 +- .../converter/SwapProviderStateConverter.kt | 2 +- .../SwapChooseProviderContentPreview.kt | 12 +- .../ui/SwapChooseTokenNetworkContent.kt | 18 -- .../swap/v2/impl/common/entity/SwapQuoteUM.kt | 6 +- .../sendviaswap/ui/SendWithSwapContent.kt | 84 +++++--- 32 files changed, 971 insertions(+), 331 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_fixed.xml create mode 100644 core/ui/src/main/res/drawable/ic_floating.xml create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountUpdateSubtitleConverter.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapFromSubtitleConverter.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapSubtitleResult.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapToSubtitleConverter.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountErrorQuoteTransformer.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt index dbc955ab1e..9fdb77d943 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldV2.kt @@ -62,6 +62,7 @@ fun AmountFieldV2( onValuePastedTriggerDismiss: () -> Unit, onCurrencyChange: (Boolean) -> Unit, modifier: Modifier = Modifier, + reserveSpaceForError: Boolean = true, ) { val decimalFormat = rememberDecimalFormat() @@ -120,12 +121,13 @@ fun AmountFieldV2( AmountSecondary( amountUM = amountUM, onCurrencyChange = onCurrencyChange, + reserveSpaceForError = reserveSpaceForError, ) } } @Composable -private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) -> Unit) { +private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) -> Unit, reserveSpaceForError: Boolean) { Box( modifier = Modifier .fillMaxWidth() @@ -144,29 +146,33 @@ private fun AmountSecondary(amountUM: AmountState, onCurrencyChange: (Boolean) - AmountFieldCurrencyInfo( amountUM = amountUM, onCurrencyChange = onCurrencyChange, + modifier = if (reserveSpaceForError) Modifier.padding(bottom = 16.dp) else Modifier, ) AmountFieldError( isError = amountUM.amountTextField.isError, isWarning = amountUM.amountTextField.isWarning, error = amountUM.amountTextField.error, + reserveSpaceForError = reserveSpaceForError, modifier = Modifier - .align(BottomCenter) - .padding(top = 24.dp), + .align(BottomCenter), ) } } } @Composable -private fun BoxScope.AmountFieldCurrencyInfo(amountUM: AmountState.Data, onCurrencyChange: (Boolean) -> Unit) { +private fun BoxScope.AmountFieldCurrencyInfo( + amountUM: AmountState.Data, + onCurrencyChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { val isFiatAvailable = amountUM.amountTextField.fiatAmount.value != null Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier + modifier = modifier .align(TopCenter) - .padding(bottom = 16.dp) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, @@ -255,13 +261,14 @@ private fun AmountFieldError( isError: Boolean, isWarning: Boolean, error: TextReference, + reserveSpaceForError: Boolean, modifier: Modifier = Modifier, ) { AnimatedVisibility( visible = isError || isWarning, enter = fadeIn(), exit = fadeOut(), - modifier = modifier, + modifier = if (reserveSpaceForError) modifier.padding(top = 24.dp) else modifier, label = "Error field appearance animation", ) { val errorText = remember(this, error) { error } @@ -271,7 +278,13 @@ private fun AmountFieldError( style = TangemTheme.typography.caption2, color = color, textAlign = TextAlign.Center, - modifier = Modifier.testTag(SendScreenTestTags.AMOUNT_ERROR_TEXT), + modifier = if (reserveSpaceForError) { + Modifier.testTag(SendScreenTestTags.AMOUNT_ERROR_TEXT) + } else { + Modifier + .padding(top = 24.dp) + .testTag(SendScreenTestTags.AMOUNT_ERROR_TEXT) + }, ) } } @@ -324,6 +337,7 @@ private fun AmountFieldV2_Preview(@PreviewParameter(AmountFieldV2PreviewProvider onValueChange = {}, onValuePastedTriggerDismiss = { }, onCurrencyChange = {}, + reserveSpaceForError = false, modifier = Modifier.background(TangemTheme.colors.background.action), ) } 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 fe59dbf317..990e4bc9c1 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 @@ -40,13 +40,13 @@ internal fun String.checkExceedBalance( maxEnterAmount: EnterAmountBoundary, amountTextField: AmountFieldModel, ): Boolean { - 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) { + val currencyFiatAmount = maxEnterAmount.fiatAmount ?: return false fiatDecimal > currencyFiatAmount } else { + val currencyCryptoAmount = maxEnterAmount.amount ?: return false cryptoDecimal > currencyCryptoAmount } } diff --git a/core/ui/src/main/res/drawable/ic_fixed.xml b/core/ui/src/main/res/drawable/ic_fixed.xml new file mode 100644 index 0000000000..585d3632c3 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_fixed.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_floating.xml b/core/ui/src/main/res/drawable/ic_floating.xml new file mode 100644 index 0000000000..0b94fd88fa --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_floating.xml @@ -0,0 +1,13 @@ + + + + diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index 0f4ef33398..f5d7042b43 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -15,6 +15,8 @@ import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.request.ExchangeSentRequestBody import com.tangem.datasource.api.express.models.request.PairsRequestBody import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails +import com.tangem.datasource.api.express.models.response.RateType +import com.tangem.datasource.api.express.models.response.SwapPairProvider import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi @@ -98,8 +100,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( } val mappedProviders = pair.providers - .filterNot { it.hasOnlyFixedRateType() } - .mapNotNull { expressProviders[it.providerId] } + .mapNotNull { it.withExpressProvider(expressProviders) } .filterYieldSupplyProvider(statusFrom) if (statusFrom != null && statusTo != null && mappedProviders.isNotEmpty()) { @@ -157,8 +158,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( val currencyStatusTo = createSendWithSwapCryptoCurrencyStatus(statusToDeferred.await()) val mappedProvider = pair.providers - .filterNot { it.hasOnlyFixedRateType() } - .mapNotNull { mappedProviders[it.providerId] } + .mapNotNull { it.withExpressProvider(mappedProviders) } .filterYieldSupplyProvider(currencyStatusFrom) if (currencyStatusFrom != null && currencyStatusTo != null && mappedProvider.isNotEmpty()) { @@ -443,6 +443,18 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( } } + private fun SwapPairProvider.withExpressProvider( + expressProviders: Map, + ): ExpressProvider? { + val provider = expressProviders[providerId] ?: return null + return provider.copy(rateTypes = rateTypes.map { it.toExpressRateType() }) + } + + private fun RateType.toExpressRateType(): ExpressRateType = when (this) { + RateType.FLOAT -> ExpressRateType.Float + RateType.FIXED -> ExpressRateType.Fixed + } + private fun CryptoCurrency.getContractAddress(): String { return when (this) { is CryptoCurrency.Token -> this.contractAddress diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt index 3a65c92b9c..45df45fc76 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt @@ -8,7 +8,7 @@ import java.math.BigDecimal * * @property provider swap provider * @property toTokenAmount amount of token you want to receive - * @property fromTokenAmount amount of from-token required (for fixed rate quotes) + * @property fromTokenAmount amount of from-token required (only set for fixed rate quotes) * @property allowanceContract whether swap occurs via third token */ data class SwapQuoteModel( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index c749636dc6..b5cdb3898e 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -10,6 +10,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.SwapRateMode import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import kotlinx.collections.immutable.ImmutableList @@ -44,6 +45,7 @@ internal sealed class SwapAmountUM { // selected swap route val swapRateType: ExpressRateType, + val swapRateMode: SwapRateMode, // swap models val swapCurrencies: SwapCurrencies, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt index 0e9ba829e8..38716f07d8 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountClickIntents.kt @@ -8,6 +8,7 @@ internal interface SwapAmountClickIntents : AmountScreenClickIntents { fun onExpandEditField(selectedAmountType: SwapAmountType) fun onInfoClick() fun onSelectTokenClick() + fun onRateClick() fun onSeparatorClick() fun onProviderClick() } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 45499a10b7..ac2fe177b4 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -16,17 +16,18 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.datasource.local.swap.SwapBestRateAnimationStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressError -import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.notifications.ShouldShowNotificationUseCase import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions +import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.swap.models.* import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase @@ -172,17 +173,43 @@ internal class SwapAmountModel @Inject constructor( secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, isNeedApplyFCARestrictions = userCountry.needApplyFCARestrictions(), + isBalanceHidden = params.isBalanceHidingFlow.value, + primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, + primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, ), ) } override fun onExpandEditField(selectedAmountType: SwapAmountType) { + val content = uiState.value as? SwapAmountUM.Content ?: return + + val newSwapRateType = when (selectedAmountType) { + SwapAmountType.To -> { + if (content.swapRateMode != SwapRateMode.FLOAT_ONLY) { + ExpressRateType.Fixed + } else { + return + } + } + SwapAmountType.From -> { + if (content.swapRateMode == SwapRateMode.FIXED_ONLY) { + ExpressRateType.Fixed + } else if (content.swapRateMode == SwapRateMode.FLOAT_AND_FIXED) { + ExpressRateType.Float + } else { + return + } + } + } + uiState.update { amountUM -> - if (amountUM !is SwapAmountUM.Content) return + if (amountUM !is SwapAmountUM.Content) return@update amountUM amountUM.copy( selectedAmountType = selectedAmountType, + swapRateType = newSwapRateType, ) } + startLoadingQuotesTask(isSilentReload = false) } override fun onInfoClick() { @@ -290,6 +317,10 @@ internal class SwapAmountModel @Inject constructor( } } + override fun onRateClick() { + // TODO [REDACTED_TASK_KEY] + } + override fun onSeparatorClick() { val amountParams = params as? SwapAmountComponentParams.AmountParams ?: return @@ -335,17 +366,9 @@ internal class SwapAmountModel @Inject constructor( private fun subscribeOnBalanceHiddenUpdates() { params.isBalanceHidingFlow.onEach { isHidden -> - val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1 uiState.transformerUpdate( SwapAmountBalanceHiddenTransformer( isBalanceHidden = isHidden, - isSingleWallet = isOnlyOneWallet, - userWallet = userWallet, - appCurrency = appCurrency, - swapDirection = swapDirection, - clickIntents = this, - isAccountsMode = params.isAccountModeFlow.value, - account = params.accountFlow.value, ), ) }.launchIn(modelScope) @@ -525,16 +548,14 @@ internal class SwapAmountModel @Inject constructor( private fun initPairs(swapCurrencies: SwapCurrencies, secondaryCryptoCurrency: CryptoCurrency?) { modelScope.launch { - val swapCryptoCurrency = selectInitialPairUseCase( + val secondaryCurrency = selectInitialPairUseCase( primaryCryptoCurrency = primaryCryptoCurrency, secondaryCryptoCurrency = secondaryCryptoCurrency, userWallet = userWallet, swapCurrencies = swapCurrencies, swapDirection = params.swapDirection, ) - - val secondaryStatus = swapCryptoCurrency?.currencyStatus - + val secondaryStatus = secondaryCurrency?.currencyStatus val primaryStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus if (secondaryStatus != null && primaryStatus != null) { initCurrencies(primaryStatus, secondaryStatus) @@ -553,8 +574,14 @@ internal class SwapAmountModel @Inject constructor( isSingleWallet = isOnlyOneWallet, isAccountsMode = params.isAccountModeFlow.value, account = params.accountFlow.value, + providers = secondaryCurrency.providers, ), ) + + val currentState = uiState.value as? SwapAmountUM.Content + if (currentState != null && currentState.swapRateMode != SwapRateMode.FLOAT_ONLY) { + computeAndSetSecondaryAmount(currentState) + } startLoadingQuotesTask(isSilentReload = false) } else { @Suppress("NullableToStringCall") @@ -592,7 +619,11 @@ internal class SwapAmountModel @Inject constructor( fiatRate = secondaryStatus.value.fiatRate, fiatAmount = secondaryStatus.value.fiatAmount, ) - secondaryMaximumAmountBoundary = MaxEnterAmountConverter().convert(secondaryStatus) + secondaryMaximumAmountBoundary = EnterAmountBoundary( + amount = null, + fiatRate = secondaryStatus.value.fiatRate, + fiatAmount = null, + ) } } @@ -606,18 +637,39 @@ internal class SwapAmountModel @Inject constructor( onReverse = { state.secondaryCryptoCurrencyStatus.currency to state.primaryCryptoCurrencyStatus.currency }, ) - val fromAmount = when (state.swapDirection) { - SwapDirection.Direct -> state.primaryAmount.amountField - SwapDirection.Reverse -> state.secondaryAmount.amountField - } as? AmountState.Data + val amountField = state.swapDirection.withSwapDirection( + onDirect = { + if (state.selectedAmountType == SwapAmountType.From) { + state.primaryAmount.amountField + } else { + state.secondaryAmount.amountField + } + }, + onReverse = { + if (state.selectedAmountType == SwapAmountType.From) { + state.secondaryAmount.amountField + } else { + state.primaryAmount.amountField + } + }, + ) as? AmountState.Data - val fromAmountValue = fromAmount?.amountTextField?.cryptoAmount?.value.orZero() + val amountValue = amountField?.amountTextField?.cryptoAmount?.value.orZero() val isAmountScreen = params is SwapAmountComponentParams.AmountParams - val isAmountError = fromAmount?.amountTextField?.isError == true || fromAmountValue.isNullOrZero() + val isAmountError = amountField?.amountTextField?.isError == true || amountValue.isNullOrZero() if (isAmountScreen && isAmountError) { uiState.transformerUpdate(SwapQuoteEmptyStateTransformer); return } + val rateType = when (state.selectedAmountType) { + SwapAmountType.To -> ExpressRateType.Fixed + SwapAmountType.From -> if (state.swapRateMode == SwapRateMode.FIXED_ONLY) { + ExpressRateType.Fixed + } else { + ExpressRateType.Float + } + } + if (!isSilentReload) uiState.transformerUpdate(SwapQuoteLoadingStateTransformer) modelScope.launch { @@ -625,6 +677,7 @@ internal class SwapAmountModel @Inject constructor( .asSequence() .filter { swapCurrencyStatus -> swapCurrencyStatus.currencyStatus.currency.id == toCryptoCurrency.id } .flatMap(SwapCryptoCurrency::providers) + .filter { provider -> provider.rateTypes.contains(rateType) } .toList() .map { provider -> async { @@ -632,9 +685,9 @@ internal class SwapAmountModel @Inject constructor( userWallet = userWallet, fromCryptoCurrency = fromCryptoCurrency, toCryptoCurrency = toCryptoCurrency, - amount = fromAmountValue, - amountType = SwapAmountType.From, - rateType = ExpressRateType.Float, + amount = amountValue, + amountType = state.selectedAmountType, + rateType = rateType, provider = provider, ).fold( ifLeft = { error -> @@ -650,7 +703,7 @@ internal class SwapAmountModel @Inject constructor( swapDirection = swapDirection, allowanceContract = quote.allowanceContract, isApprovalNeeded = checkAllowance(state, quote), - fromAmount = fromAmountValue, + fromAmount = amountValue, ).convert( SwapQuoteUMConverter.Data( quote = quote, @@ -669,12 +722,48 @@ internal class SwapAmountModel @Inject constructor( secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, isSilentReload = isSilentReload, isNeedApplyFcaRestrictions = userCountry.needApplyFCARestrictions(), + isBalanceHidden = params.isBalanceHidingFlow.value, + primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, + primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, ), ) feeSelectorReloadTrigger.triggerUpdate() } } + /** + * Compute secondary amount from primary amount using fiat rate conversion + * secondaryAmount = primaryAmount × primaryFiatRate / secondaryFiatRate + * + */ + private fun computeAndSetSecondaryAmount(state: SwapAmountUM.Content) { + val secondaryStatus = state.secondaryCryptoCurrencyStatus ?: return + val primaryFiatRate = state.primaryCryptoCurrencyStatus.value.fiatRate ?: return + val secondaryFiatRate = secondaryStatus.value.fiatRate ?: return + if (secondaryFiatRate <= BigDecimal.ZERO) return + + val primaryAmountField = (state.primaryAmount as? SwapAmountFieldUM.Content) + ?.amountField as? AmountState.Data ?: return + val primaryAmount = primaryAmountField.amountTextField.cryptoAmount.value ?: return + if (primaryAmount <= BigDecimal.ZERO) return + + val secondaryAmount = primaryAmount + .multiply(primaryFiatRate) + .divide(secondaryFiatRate, secondaryStatus.currency.decimals, java.math.RoundingMode.HALF_UP) + + val secondaryValue = secondaryAmount.parseBigDecimal(secondaryStatus.currency.decimals) + + uiState.transformerUpdate( + SwapAmountValueChangeTransformer( + primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, + secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, + primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, + secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, + value = secondaryValue, + ), + ) + } + private fun startLoadingQuotesTask(isSilentReload: Boolean) { quoteTaskScheduler.cancelTask() loadQuotes(isSilentReload = isSilentReload) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt index f3b1614ce2..f0fee9face 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt @@ -58,9 +58,9 @@ internal object SwapAmountQuoteUtils { ): SwapAmountUM { if (this !is SwapAmountUM.Content) return this - val updatedAmountField = if ( - selectedAmountType == SwapAmountType.From && swapDirection == SwapDirection.Direct - ) { + val isPrimaryFieldEdited = selectedAmountType == SwapAmountType.From && swapDirection == SwapDirection.Direct + + val updatedAmountField = if (isPrimaryFieldEdited) { val amountFieldUM = primaryAmount as? SwapAmountFieldUM.Content ?: return this amountFieldUM.onPrimaryAmount(primaryCryptoCurrencyStatus) } else { @@ -68,9 +68,9 @@ internal object SwapAmountQuoteUtils { val amountFieldUM = secondaryAmount as? SwapAmountFieldUM.Content ?: return this amountFieldUM.onSecondaryAmount(secondaryCryptoCurrencyStatus) } - return copy( - primaryAmount = updatedAmountField, + primaryAmount = if (isPrimaryFieldEdited) updatedAmountField else primaryAmount, + secondaryAmount = if (isPrimaryFieldEdited) secondaryAmount else updatedAmountField, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt index 9adad0ddeb..6bfb4de5bd 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt @@ -5,12 +5,8 @@ import com.tangem.common.ui.amountScreen.converters.AmountAccountConverter 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.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.* -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.models.account.Account import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -19,7 +15,7 @@ import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM -import com.tangem.utils.StringsSigns.DOT +import java.math.BigDecimal @Suppress("LongParameterList") internal class SwapAmountFieldConverter( @@ -36,24 +32,34 @@ internal class SwapAmountFieldConverter( private val iconStateConverter = CryptoCurrencyToIconStateConverter() private val maxEnterAmountConverter = MaxEnterAmountConverter() - fun convert(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus): SwapAmountFieldUM { + fun convert( + swapAmountType: SwapAmountType, + cryptoCurrencyStatus: CryptoCurrencyStatus, + isSelected: Boolean, + isAmountEmpty: Boolean = true, + displayAmount: BigDecimal? = null, + ): SwapAmountFieldUM { val walletTitle = if (isSingleWallet) { resourceReference(R.string.send_from_title) } else { resourceReference(R.string.send_from_wallet_name, wrappedList(userWallet.name)) } + val subtitles = computeSubtitles( + swapAmountType = swapAmountType, + cryptoCurrencyStatus = cryptoCurrencyStatus, + isEntering = isSelected, + isAmountEmpty = isAmountEmpty, + displayAmount = displayAmount, + ) return SwapAmountFieldUM.Content( - amountType = selectedType, + amountType = swapAmountType, title = stringReference(cryptoCurrencyStatus.currency.name), - subtitleLeft = getSubtitleLeft(selectedType = selectedType, cryptoCurrencyStatus = cryptoCurrencyStatus), - subtitleEllipsisLeft = getSubtitleEllipsisLeft( - selectedType = selectedType, - cryptoCurrencyStatus = cryptoCurrencyStatus, - ), - subtitleRight = getSubtitleRight(selectedType = selectedType, cryptoCurrencyStatus = cryptoCurrencyStatus), - subtitleEllipsisRight = TextEllipsis.OffsetEnd(appCurrency.symbol.length), + subtitleLeft = subtitles.subtitleLeft, + subtitleEllipsisLeft = subtitles.subtitleEllipsisLeft, + subtitleRight = subtitles.subtitleRight, + subtitleEllipsisRight = subtitles.subtitleEllipsisRight, priceImpact = null, - isClickEnabled = selectedType.isViewingField(), + isClickEnabled = true, amountField = AmountStateConverter( clickIntents = clickIntents, appCurrency = appCurrency, @@ -65,8 +71,8 @@ internal class SwapAmountFieldConverter( isAccountsMode = isAccountsMode, walletTitle = walletTitle, prefixText = when { - selectedType.isEnteringField() -> resourceReference(R.string.common_from) - selectedType.isViewingField() -> resourceReference(R.string.common_to) + swapAmountType.isEnteringField() -> resourceReference(R.string.common_from) + swapAmountType.isViewingField() -> resourceReference(R.string.common_to) else -> TextReference.Companion.EMPTY }, ).convert(account), @@ -79,44 +85,29 @@ internal class SwapAmountFieldConverter( ) } - private fun getSubtitleLeft(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus) = when { - selectedType.isEnteringField() -> combinedReference( - stringReference( - cryptoCurrencyStatus.value.amount.format { - crypto(cryptoCurrency = cryptoCurrencyStatus.currency) - }, - ), - ).orMaskWithStars(isBalanceHidden) - selectedType.isViewingField() -> resourceReference(R.string.send_with_swap_recipient_get_amount) - else -> TextReference.Companion.EMPTY + private fun computeSubtitles( + swapAmountType: SwapAmountType, + cryptoCurrencyStatus: CryptoCurrencyStatus, + isEntering: Boolean, + isAmountEmpty: Boolean, + displayAmount: BigDecimal?, + ): SwapSubtitleResult = when (swapAmountType) { + SwapAmountType.From -> SwapFromSubtitleConverter.convert( + cryptoCurrencyStatus = cryptoCurrencyStatus, + isBalanceHidden = isBalanceHidden, + isEntering = isEntering, + isAmountEmpty = isAmountEmpty, + displayAmount = displayAmount, + ) + SwapAmountType.To -> SwapToSubtitleConverter.convert( + cryptoCurrencyStatus = cryptoCurrencyStatus, + isBalanceHidden = isBalanceHidden, + isEntering = isEntering, + isAmountEmpty = isAmountEmpty, + displayAmount = displayAmount, + ) } - private fun getSubtitleRight(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus) = when { - selectedType.isEnteringField() -> if (isBalanceHidden) { - TextReference.EMPTY - } else { - combinedReference( - stringReference(value = " $DOT "), - stringReference( - cryptoCurrencyStatus.value.fiatAmount.format { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - }, - ), - ) - } - else -> TextReference.EMPTY - } - - private fun getSubtitleEllipsisLeft(selectedType: SwapAmountType, cryptoCurrencyStatus: CryptoCurrencyStatus) = - when { - selectedType.isEnteringField() -> TextEllipsis.OffsetEnd(cryptoCurrencyStatus.currency.symbol.length) - selectedType.isViewingField() -> TextEllipsis.End - else -> TextEllipsis.End - } - private fun SwapAmountType.isEnteringField(): Boolean { return this == SwapAmountType.From && swapDirection == SwapDirection.Direct || this == SwapAmountType.To && swapDirection == SwapDirection.Reverse diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountUpdateSubtitleConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountUpdateSubtitleConverter.kt new file mode 100644 index 0000000000..1ed8a5c867 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountUpdateSubtitleConverter.kt @@ -0,0 +1,63 @@ +package com.tangem.features.swap.v2.impl.amount.model.converter + +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.swap.models.SwapAmountType +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import java.math.BigDecimal + +/** + * Updates all subtitle fields on an existing [SwapAmountFieldUM.Content] field. + * + * Delegates to [SwapFromSubtitleConverter] / [SwapToSubtitleConverter] based on the field's + * [SwapAmountFieldUM.Content.amountType]. All other field properties are preserved. + * + * `isEntering` is derived internally: the field is "entering" when its amountType matches + * [selectedAmountType], so callers cannot accidentally pass a wrong value. + * + * @param selectedAmountType the currently selected amount type from [SwapAmountUM.Content] + * @param isBalanceHidden whether balance values should be masked + */ +internal class SwapAmountUpdateSubtitleConverter( + private val selectedAmountType: SwapAmountType, + private val isBalanceHidden: Boolean, +) { + + /** + * Updates all subtitle fields after a quote arrives. + * + * @param field the current field content to update + * @param cryptoCurrencyStatus status used for balance formatting + * @param isAmountEmpty whether the amount in this field is empty/null + * @param displayAmount optional override for the displayed amount (e.g. quote toAmount/fromAmount) + */ + fun updateSubtitles( + field: SwapAmountFieldUM.Content, + cryptoCurrencyStatus: CryptoCurrencyStatus, + isAmountEmpty: Boolean, + displayAmount: BigDecimal? = null, + ): SwapAmountFieldUM.Content { + val isEntering = field.amountType == selectedAmountType + val subtitles = when (field.amountType) { + SwapAmountType.From -> SwapFromSubtitleConverter.convert( + cryptoCurrencyStatus = cryptoCurrencyStatus, + isBalanceHidden = isBalanceHidden, + isEntering = isEntering, + isAmountEmpty = isAmountEmpty, + displayAmount = displayAmount, + ) + SwapAmountType.To -> SwapToSubtitleConverter.convert( + cryptoCurrencyStatus = cryptoCurrencyStatus, + isBalanceHidden = isBalanceHidden, + isEntering = isEntering, + isAmountEmpty = isAmountEmpty, + displayAmount = displayAmount, + ) + } + return field.copy( + subtitleLeft = subtitles.subtitleLeft, + subtitleEllipsisLeft = subtitles.subtitleEllipsisLeft, + subtitleRight = subtitles.subtitleRight, + subtitleEllipsisRight = subtitles.subtitleEllipsisRight, + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapFromSubtitleConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapFromSubtitleConverter.kt new file mode 100644 index 0000000000..ed5c112fd0 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapFromSubtitleConverter.kt @@ -0,0 +1,69 @@ +package com.tangem.features.swap.v2.impl.amount.model.converter + +import com.tangem.core.ui.components.atoms.text.TextEllipsis +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.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.swap.v2.impl.R +import java.math.BigDecimal + +/** + * Computes all subtitle fields for the **From** (primary) field. + * + * | State | subtitleLeft | subtitleRight | ellipsisLeft | + * |--------------------------------|-----------------------------------|-----------------------------------|------------------------| + * | Entering (float), any | "Balance: " (empty param) | "{balance}" masked | OffsetEnd(symbol) | + * | Viewing (fixed), empty | "Balance: " (empty param) | "{balance}" masked (crypto only) | End | + * | Viewing (fixed), not empty | send_from_title | "{displayStr}" masked | OffsetEnd(symbol) | + */ +internal object SwapFromSubtitleConverter { + + fun convert( + cryptoCurrencyStatus: CryptoCurrencyStatus, + isBalanceHidden: Boolean, + isEntering: Boolean, + isAmountEmpty: Boolean, + displayAmount: BigDecimal?, + ): SwapSubtitleResult { + val symbol = cryptoCurrencyStatus.currency.symbol + val balance = cryptoCurrencyStatus.value.amount.format { + crypto(cryptoCurrency = cryptoCurrencyStatus.currency) + } + val displayStr = displayAmount?.format { + crypto(cryptoCurrency = cryptoCurrencyStatus.currency) + } ?: balance + + val subtitleLeft: TextReference + val subtitleRight: TextReference + val ellipsisLeft: TextEllipsis + + when { + isEntering -> { + subtitleLeft = resourceReference(R.string.common_balance, wrappedList("")) + subtitleRight = combinedReference(stringReference(balance)) + .orMaskWithStars(isBalanceHidden) + ellipsisLeft = TextEllipsis.OffsetEnd(symbol.length) + } + !isEntering && isAmountEmpty -> { + subtitleLeft = resourceReference(R.string.common_balance, wrappedList("")) + subtitleRight = combinedReference(stringReference(balance)) + .orMaskWithStars(isBalanceHidden) + ellipsisLeft = TextEllipsis.End + } + else -> { + subtitleLeft = resourceReference(R.string.send_from_title) + subtitleRight = combinedReference(stringReference(displayStr)) + .orMaskWithStars(isBalanceHidden) + ellipsisLeft = TextEllipsis.OffsetEnd(symbol.length) + } + } + + return SwapSubtitleResult( + subtitleLeft = subtitleLeft, + subtitleRight = subtitleRight, + subtitleEllipsisLeft = ellipsisLeft, + subtitleEllipsisRight = TextEllipsis.OffsetEnd(symbol.length), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt index c014e539e7..3f5255f097 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt @@ -42,6 +42,10 @@ internal class SwapQuoteUMConverter( append(rate.format { crypto(secondaryCurrency) }) } + val fromAmountValue = stringReference( + quote.fromTokenAmount?.format { crypto(primaryCurrency) }.orEmpty(), + ) + return if (allowanceContract != null) { if (isApprovalNeeded) { SwapQuoteUM.Allowance( @@ -51,11 +55,13 @@ internal class SwapQuoteUMConverter( } else { SwapQuoteUM.Content( provider = provider, - quoteAmount = quote.toTokenAmount, + toAmount = quote.toTokenAmount, + fromAmount = quote.fromTokenAmount, diffPercent = DifferencePercent.Empty, - quoteAmountValue = stringReference( + toAmountValue = stringReference( quote.toTokenAmount.toQuoteValue(), ), + fromAmountValue = fromAmountValue, rate = annotatedReference(rateString), isSingleProvider = false, ) @@ -63,11 +69,13 @@ internal class SwapQuoteUMConverter( } else { SwapQuoteUM.Content( provider = provider, - quoteAmount = quote.toTokenAmount, + toAmount = quote.toTokenAmount, + fromAmount = quote.fromTokenAmount, diffPercent = DifferencePercent.Empty, - quoteAmountValue = stringReference( + toAmountValue = stringReference( quote.toTokenAmount.toQuoteValue(), ), + fromAmountValue = fromAmountValue, rate = annotatedReference(rateString), isSingleProvider = false, ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapSubtitleResult.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapSubtitleResult.kt new file mode 100644 index 0000000000..3253f56f44 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapSubtitleResult.kt @@ -0,0 +1,11 @@ +package com.tangem.features.swap.v2.impl.amount.model.converter + +import com.tangem.core.ui.components.atoms.text.TextEllipsis +import com.tangem.core.ui.extensions.TextReference + +internal data class SwapSubtitleResult( + val subtitleLeft: TextReference, + val subtitleRight: TextReference, + val subtitleEllipsisLeft: TextEllipsis, + val subtitleEllipsisRight: TextEllipsis, +) \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapToSubtitleConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapToSubtitleConverter.kt new file mode 100644 index 0000000000..cba9789884 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapToSubtitleConverter.kt @@ -0,0 +1,73 @@ +package com.tangem.features.swap.v2.impl.amount.model.converter + +import com.tangem.core.ui.components.atoms.text.TextEllipsis +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.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.swap.v2.impl.R +import com.tangem.utils.StringsSigns.TILDE_SIGN +import java.math.BigDecimal + +/** + * Computes all subtitle fields for the **To** (secondary) field. + * + * | State | subtitleLeft | subtitleRight | ellipsisLeft | + * |--------------------------------|------------------------------------|----------------------------|------------------------| + * | Viewing (float), empty | "Will be sent to recipient" | EMPTY | End | + * | Viewing (float), not empty | "Recipient gets" (empty param) | "~{displayStr}" masked | End | + * | Entering (fixed), any | "Will be sent to recipient" | EMPTY | End | + */ +internal object SwapToSubtitleConverter { + + fun convert( + cryptoCurrencyStatus: CryptoCurrencyStatus, + isBalanceHidden: Boolean, + isEntering: Boolean, + isAmountEmpty: Boolean, + displayAmount: BigDecimal?, + ): SwapSubtitleResult { + val symbol = cryptoCurrencyStatus.currency.symbol + val balance = cryptoCurrencyStatus.value.amount.format { + crypto(cryptoCurrency = cryptoCurrencyStatus.currency) + } + val displayStr = displayAmount?.format { + crypto(cryptoCurrency = cryptoCurrencyStatus.currency) + } ?: balance + + val subtitleLeft: TextReference + val subtitleRight: TextReference + val ellipsisLeft: TextEllipsis + + when { + isEntering -> { + subtitleLeft = resourceReference(R.string.send_amount_receive_token_subtitle) + subtitleRight = TextReference.EMPTY + ellipsisLeft = TextEllipsis.End + } + !isEntering && isAmountEmpty -> { + subtitleLeft = resourceReference(R.string.send_amount_receive_token_subtitle) + subtitleRight = TextReference.EMPTY + ellipsisLeft = TextEllipsis.End + } + else -> { + subtitleLeft = resourceReference( + R.string.send_with_swap_recipient_get_amount, + wrappedList(""), + ) + subtitleRight = combinedReference( + stringReference(TILDE_SIGN), + stringReference(displayStr), + ).orMaskWithStars(isBalanceHidden) + ellipsisLeft = TextEllipsis.End + } + } + + return SwapSubtitleResult( + subtitleLeft = subtitleLeft, + subtitleRight = subtitleRight, + subtitleEllipsisLeft = ellipsisLeft, + subtitleEllipsisRight = TextEllipsis.OffsetEnd(symbol.length), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt index ede7a349fb..1fe1635001 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt @@ -1,70 +1,55 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers -import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.account.Account -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.swap.models.SwapAmountType -import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM -import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter +import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountUpdateSubtitleConverter +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.utils.transformer.Transformer -@Suppress("LongParameterList") internal class SwapAmountBalanceHiddenTransformer( private val isBalanceHidden: Boolean, - private val isSingleWallet: Boolean, - private val userWallet: UserWallet, - private val appCurrency: AppCurrency, - private val swapDirection: SwapDirection, - private val clickIntents: AmountScreenClickIntents, - private val isAccountsMode: Boolean, - private val account: Account?, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { val content = prevState as? SwapAmountUM.Content ?: return prevState - val amountFieldConverter = SwapAmountFieldConverter( - swapDirection = swapDirection, + val quoteContent = content.selectedQuote as? SwapQuoteUM.Content + + val subtitleConverter = SwapAmountUpdateSubtitleConverter( + selectedAmountType = content.selectedAmountType, isBalanceHidden = isBalanceHidden, - userWallet = userWallet, - appCurrency = appCurrency, - clickIntents = clickIntents, - isSingleWallet = isSingleWallet, - isAccountsMode = isAccountsMode, - account = account, ) - val recalculatedPrimary = amountFieldConverter.convert( - selectedType = SwapAmountType.From, - cryptoCurrencyStatus = content.primaryCryptoCurrencyStatus, - ) as SwapAmountFieldUM.Content - - val oldPrimary = content.primaryAmount as? SwapAmountFieldUM.Content - - val mergedAmountField = if ( - oldPrimary?.amountField is AmountState.Data && recalculatedPrimary.amountField is AmountState.Data - ) { - val oldData = oldPrimary.amountField - val newData = recalculatedPrimary.amountField - newData.copy( - amountTextField = oldData.amountTextField, - isPrimaryButtonEnabled = oldData.isPrimaryButtonEnabled, - isEditingDisabled = oldData.isEditingDisabled, - reduceAmountBy = oldData.reduceAmountBy, - isIgnoreReduce = oldData.isIgnoreReduce, + val updatedPrimary = (content.primaryAmount as? SwapAmountFieldUM.Content)?.let { primary -> + val isAmountEmpty = (primary.amountField as? AmountState.Data) + ?.amountTextField?.cryptoAmount?.value == null + subtitleConverter.updateSubtitles( + field = primary, + cryptoCurrencyStatus = content.primaryCryptoCurrencyStatus, + isAmountEmpty = isAmountEmpty, + displayAmount = quoteContent?.fromAmount, ) + } ?: content.primaryAmount + + val updatedSecondary = if (content.secondaryCryptoCurrencyStatus != null) { + (content.secondaryAmount as? SwapAmountFieldUM.Content)?.let { secondary -> + val isAmountEmpty = (secondary.amountField as? AmountState.Data) + ?.amountTextField?.cryptoAmount?.value == null + subtitleConverter.updateSubtitles( + field = secondary, + cryptoCurrencyStatus = content.secondaryCryptoCurrencyStatus, + isAmountEmpty = isAmountEmpty, + displayAmount = quoteContent?.toAmount, + ) + } ?: content.secondaryAmount } else { - recalculatedPrimary.amountField + content.secondaryAmount } - val updatedPrimaryAmount = recalculatedPrimary.copy( - amountField = mergedAmountField, + return content.copy( + primaryAmount = updatedPrimary, + secondaryAmount = updatedSecondary, ) - - return content.copy(primaryAmount = updatedPrimaryAmount) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountErrorQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountErrorQuoteTransformer.kt new file mode 100644 index 0000000000..edb2d9e257 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountErrorQuoteTransformer.kt @@ -0,0 +1,58 @@ +package com.tangem.features.swap.v2.impl.amount.model.transformers + +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.domain.swap.models.SwapAmountType +import com.tangem.utils.transformer.Transformer + +/** + * Sets a generic "Something went wrong" error on the selected amount field + * when all quotes returned errors. + */ +internal object SwapAmountErrorQuoteTransformer : Transformer { + + override fun transform(prevState: SwapAmountUM): SwapAmountUM { + if (prevState !is SwapAmountUM.Content) return prevState + + val error = resourceReference(R.string.send_with_swap_something_went_wrong) + + val isPrimarySelected = prevState.selectedAmountType == SwapAmountType.From + + val newPrimaryAmount = if (isPrimarySelected) { + applyError(prevState.primaryAmount, error) + } else { + prevState.primaryAmount + } + + val newSecondaryAmount = if (!isPrimarySelected) { + applyError(prevState.secondaryAmount, error) + } else { + prevState.secondaryAmount + } + + return prevState.copy( + isPrimaryButtonEnabled = false, + primaryAmount = newPrimaryAmount, + secondaryAmount = newSecondaryAmount, + ) + } + + private fun applyError( + field: SwapAmountFieldUM, + error: com.tangem.core.ui.extensions.TextReference, + ): SwapAmountFieldUM { + val content = field as? SwapAmountFieldUM.Content ?: return field + val amountData = content.amountField as? AmountState.Data ?: return field + return content.copy( + amountField = amountData.copy( + amountTextField = amountData.amountTextField.copy( + error = error, + isError = true, + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt index bb5046a48d..e2d648a6c9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt @@ -9,6 +9,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.SwapRateMode import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter @@ -46,8 +47,9 @@ internal class SwapAmountPrimaryReadyStateTransformer( isPrimaryButtonEnabled = false, primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, primaryAmount = amountFieldConverter.convert( - selectedType = SwapAmountType.From, + swapAmountType = SwapAmountType.From, cryptoCurrencyStatus = primaryCryptoCurrencyStatus, + isSelected = prevState.selectedAmountType == SwapAmountType.From, ), secondaryCryptoCurrencyStatus = null, secondaryAmount = SwapAmountFieldUM.Empty( @@ -62,6 +64,7 @@ internal class SwapAmountPrimaryReadyStateTransformer( appCurrency = appCurrency, isShowBestRateAnimation = isShowBestRateAnimation, isShowFCAWarning = false, + swapRateMode = (prevState as? SwapAmountUM.Content)?.swapRateMode ?: SwapRateMode.FLOAT_ONLY, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt index 545c13db6e..cc49a10c46 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt @@ -2,13 +2,15 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.SwapAmountType +import com.tangem.domain.swap.models.SwapRateMode import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM @@ -20,6 +22,7 @@ internal class SwapAmountSecondaryReadyStateTransformer( private val userWallet: UserWallet, private val primaryCryptoCurrencyStatus: CryptoCurrencyStatus, private val secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, + private val providers: List, private val appCurrency: AppCurrency, private val swapCurrencies: SwapCurrencies, private val clickIntents: AmountScreenClickIntents, @@ -43,24 +46,41 @@ internal class SwapAmountSecondaryReadyStateTransformer( ) override fun transform(prevState: SwapAmountUM): SwapAmountUM { + val rateTypes = providers.flatMapTo(mutableSetOf()) { it.rateTypes } + val swapRateMode = when { + rateTypes.containsAll(listOf(ExpressRateType.Float, ExpressRateType.Fixed)) -> SwapRateMode.FLOAT_AND_FIXED + rateTypes.contains(ExpressRateType.Fixed) -> SwapRateMode.FIXED_ONLY + else -> SwapRateMode.FLOAT_ONLY + } + val selectedAmountType = if (swapRateMode != SwapRateMode.FLOAT_ONLY) { + ExpressRateType.Fixed + } else { + ExpressRateType.Float + } return SwapAmountUM.Content( isPrimaryButtonEnabled = false, primaryAmount = prevState.primaryAmount, primaryCryptoCurrencyStatus = primaryCryptoCurrencyStatus, secondaryAmount = amountFieldConverter.convert( - selectedType = SwapAmountType.To, + swapAmountType = SwapAmountType.To, cryptoCurrencyStatus = secondaryCryptoCurrencyStatus, + isSelected = prevState.selectedAmountType == SwapAmountType.To, ), secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, swapCurrencies = swapCurrencies, - selectedAmountType = prevState.selectedAmountType, + selectedAmountType = if (selectedAmountType == ExpressRateType.Fixed) { + SwapAmountType.To + } else { + SwapAmountType.From + }, swapDirection = swapDirection, - swapRateType = ExpressRateType.Float, + swapRateType = selectedAmountType, swapQuotes = persistentListOf(), selectedQuote = SwapQuoteUM.Empty, appCurrency = appCurrency, isShowBestRateAnimation = isShowBestRateAnimation, isShowFCAWarning = false, + swapRateMode = swapRateMode, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt index 3f3f9ac158..8e1878c21c 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt @@ -4,12 +4,15 @@ import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTrans import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.calculatePriceImpact import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountErrorConverter +import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountUpdateSubtitleConverter +import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA import com.tangem.utils.extensions.orZero @@ -20,63 +23,162 @@ internal class SwapAmountSelectQuoteTransformer( private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, private val isNeedApplyFCARestrictions: Boolean, + private val isBalanceHidden: Boolean, + private val primaryMaximumAmountBoundary: EnterAmountBoundary? = null, + private val primaryMinimumAmountBoundary: EnterAmountBoundary? = null, ) : Transformer { + + @Suppress("CyclomaticComplexMethod", "LongMethod") override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState - val providerErrorConverter = SwapAmountErrorConverter( + val isPrimarySelected = prevState.selectedAmountType == SwapAmountType.From + val isSecondarySelected = prevState.selectedAmountType == SwapAmountType.To + + val primaryProviderErrorConverter = SwapAmountErrorConverter( cryptoCurrency = prevState.primaryCryptoCurrencyStatus.currency, ) + val secondaryProviderErrorConverter = prevState.secondaryCryptoCurrencyStatus?.let { + SwapAmountErrorConverter(cryptoCurrency = it.currency) + } - return prevState.copy( - isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, - selectedQuote = quoteUM, - isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, - primaryAmount = if (prevState.selectedAmountType == SwapAmountType.From) { - val swapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content - val amountField = swapAmountField?.amountField as? AmountState.Data + val quoteContent = quoteUM as? SwapQuoteUM.Content + val fromAmount = quoteContent?.fromAmount + val toAmount = quoteContent?.toAmount - val amountError = (quoteUM as? SwapQuoteUM.Error)?.expressError?.let(providerErrorConverter::convert) + val primarySwapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content + val secondarySwapAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content - swapAmountField?.copy( + val subtitleConverter = SwapAmountUpdateSubtitleConverter( + selectedAmountType = prevState.selectedAmountType, + isBalanceHidden = isBalanceHidden, + ) + + val newPrimaryAmount = when { + fromAmount != null && primaryMaximumAmountBoundary != null -> { + primarySwapAmountField?.let { fromField -> + subtitleConverter.updateSubtitles( + field = fromField, + cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + isAmountEmpty = false, + displayAmount = fromAmount, + ).copy( + amountField = AmountFieldChangeTransformer( + cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + maxEnterAmount = primaryMaximumAmountBoundary, + minimumTransactionAmount = primaryMinimumAmountBoundary, + value = fromAmount.parseBigDecimal( + prevState.primaryCryptoCurrencyStatus.currency.decimals, + ), + ).transform(fromField.amountField), + ) + } ?: prevState.primaryAmount + } + isPrimarySelected -> { + val amountField = primarySwapAmountField?.amountField as? AmountState.Data + val amountError = (quoteUM as? SwapQuoteUM.Error)?.expressError + ?.let(primaryProviderErrorConverter::convert) + primarySwapAmountField?.copy( amountField = amountField?.copy( amountTextField = amountField.amountTextField.copy( error = amountError ?: TextReference.EMPTY, isError = amountError != null, ), - ) ?: swapAmountField.amountField, + ) ?: primarySwapAmountField.amountField, ) ?: prevState.primaryAmount - } else { - prevState.primaryAmount - }, - secondaryAmount = if (prevState.selectedAmountType == SwapAmountType.From && - prevState.secondaryCryptoCurrencyStatus != null && secondaryMaximumAmountBoundary != null - ) { - val secondaryAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content - val fromAmount = (prevState.primaryAmount.amountField as? AmountState.Data) - ?.amountTextField?.cryptoAmount?.value.orZero() - val toAmount = (quoteUM as? SwapQuoteUM.Content)?.quoteAmount - val priceImpact = calculatePriceImpact( - swapDirection = prevState.swapDirection, - fromTokenAmount = fromAmount, - toTokenAmount = toAmount.orZero(), - primaryCryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, - secondaryCryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, - ) - - secondaryAmountField?.copy( - priceImpact = priceImpact, + } + !isPrimarySelected && primarySwapAmountField != null && primaryMaximumAmountBoundary != null -> { + subtitleConverter.updateSubtitles( + field = primarySwapAmountField, + cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + isAmountEmpty = true, + ).copy( amountField = AmountFieldChangeTransformer( - cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, - maxEnterAmount = secondaryMaximumAmountBoundary, - minimumTransactionAmount = secondaryMinimumAmountBoundary, - value = toAmount?.parseBigDecimal(prevState.secondaryCryptoCurrencyStatus.currency.decimals) - .orEmpty(), - ).transform(secondaryAmountField.amountField), - ) ?: prevState.secondaryAmount + cryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + maxEnterAmount = primaryMaximumAmountBoundary, + minimumTransactionAmount = primaryMinimumAmountBoundary, + value = "", + ).transform(primarySwapAmountField.amountField), + ) + } + else -> prevState.primaryAmount + } + + val newSecondaryAmount = if ( + prevState.secondaryCryptoCurrencyStatus != null && + secondaryMaximumAmountBoundary != null && + secondarySwapAmountField != null + ) { + val fromAmountForPriceImpact = fromAmount + ?: (prevState.primaryAmount.amountField as? AmountState.Data) + ?.amountTextField?.cryptoAmount?.value.orZero() + val priceImpact = calculatePriceImpact( + swapDirection = prevState.swapDirection, + fromTokenAmount = fromAmountForPriceImpact, + toTokenAmount = toAmount.orZero(), + primaryCryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, + ) + val isAmountEmpty = toAmount == null + val secondaryAmountError = if (isSecondarySelected) { + (quoteUM as? SwapQuoteUM.Error)?.expressError + ?.let { secondaryProviderErrorConverter?.convert(it) } } else { - prevState.secondaryAmount - }, + null + } + val transformedSecondaryAmountField = if (isSecondarySelected && toAmount == null) { + secondarySwapAmountField.amountField + } else { + AmountFieldChangeTransformer( + cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, + maxEnterAmount = secondaryMaximumAmountBoundary, + minimumTransactionAmount = secondaryMinimumAmountBoundary, + value = toAmount?.parseBigDecimal(prevState.secondaryCryptoCurrencyStatus.currency.decimals) + .orEmpty(), + ).transform(secondarySwapAmountField.amountField) + } + val insufficientFundsError = if (isSecondarySelected && fromAmount != null) { + val primaryBalance = prevState.primaryCryptoCurrencyStatus.value.amount + if (primaryBalance != null && fromAmount > primaryBalance) { + resourceReference(R.string.swapping_insufficient_funds) + } else { + null + } + } else { + null + } + val effectiveSecondaryError = secondaryAmountError ?: insufficientFundsError + val secondaryAmountFieldWithError = if (isSecondarySelected) { + (transformedSecondaryAmountField as? AmountState.Data)?.let { data -> + data.copy( + amountTextField = data.amountTextField.copy( + error = effectiveSecondaryError ?: TextReference.EMPTY, + isError = effectiveSecondaryError != null, + ), + ) + } ?: transformedSecondaryAmountField + } else { + transformedSecondaryAmountField + } + subtitleConverter.updateSubtitles( + field = secondarySwapAmountField, + cryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, + isAmountEmpty = isAmountEmpty, + displayAmount = toAmount, + ).copy( + priceImpact = priceImpact, + amountField = secondaryAmountFieldWithError, + ) + } else { + prevState.secondaryAmount + } + + return prevState.copy( + isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, + selectedQuote = quoteUM, + isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, + primaryAmount = newPrimaryAmount, + secondaryAmount = newSecondaryAmount, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index cd19623928..9349099eb2 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent @@ -23,18 +24,29 @@ internal class SwapAmountSetQuotesTransformer( private val secondaryMinimumAmountBoundary: EnterAmountBoundary?, private val isSilentReload: Boolean, private val isNeedApplyFcaRestrictions: Boolean, + private val isBalanceHidden: Boolean, + private val primaryMaximumAmountBoundary: EnterAmountBoundary? = null, + private val primaryMinimumAmountBoundary: EnterAmountBoundary? = null, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState + val selectedAmountType = prevState.selectedAmountType + val comparator = SwapQuotesComparator(selectedAmountType) + val isSingleProvider = quotes.filter { swapQuoteUM -> swapQuoteUM is SwapQuoteUM.Content || swapQuoteUM is SwapQuoteUM.Allowance || (swapQuoteUM as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError }.isSingleItem() - val sortedQuotes = quotes.sortedWith(SwapQuotesComparator) - val bestQuote = findBestQuote(quotes) ?: SwapQuoteUM.Empty - val quotesWithDiff = getQuotesWithDiff(sortedQuotes, bestQuote, isSingleProvider) + val sortedQuotes = quotes.sortedWith(comparator) + val bestQuote = findBestQuote(quotes, comparator) ?: SwapQuoteUM.Empty + val quotesWithDiff = getQuotesWithDiff( + sortedQuotes = sortedQuotes, + bestQuote = bestQuote, + isSingleProvider = isSingleProvider, + selectedAmountType = selectedAmountType, + ) val selectedQuote = if (isSilentReload && prevState.selectedQuote !is SwapQuoteUM.Loading) { quotesWithDiff.firstOrNull { it.provider?.providerId == prevState.selectedQuote.provider?.providerId } ?: prevState.selectedQuote @@ -51,14 +63,30 @@ internal class SwapAmountSetQuotesTransformer( secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, isNeedApplyFCARestrictions = isNeedApplyFcaRestrictions && selectedQuote.provider?.isRestrictedByFCA() == true, + isBalanceHidden = isBalanceHidden, + primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, + primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, ) val updatedState = selectQuoteTransformer.transform(prevState = prevState) if (updatedState !is SwapAmountUM.Content) return prevState - return updatedState.copy( - isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled && quotesWithDiff.isNotEmpty(), - swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote, isSingleProvider), + val areAllQuotesErrors = quotes.all { it is SwapQuoteUM.Error } + val stateWithError = if (areAllQuotesErrors) { + SwapAmountErrorQuoteTransformer.transform(updatedState) + } else { + updatedState + } + if (stateWithError !is SwapAmountUM.Content) return prevState + + return stateWithError.copy( + isPrimaryButtonEnabled = stateWithError.isPrimaryButtonEnabled && quotesWithDiff.isNotEmpty(), + swapQuotes = getQuotesWithDiff( + sortedQuotes = sortedQuotes, + bestQuote = bestQuote, + isSingleProvider = isSingleProvider, + selectedAmountType = selectedAmountType, + ), ) } @@ -66,54 +94,75 @@ internal class SwapAmountSetQuotesTransformer( sortedQuotes: List, bestQuote: SwapQuoteUM, isSingleProvider: Boolean, + selectedAmountType: SwapAmountType, ): ImmutableList { - return sortedQuotes.sortedWith(SwapQuotesComparator) - .map { quote -> - if (quote is SwapQuoteUM.Content && bestQuote is SwapQuoteUM.Content) { - if (quote.provider.providerId == bestQuote.provider.providerId) { - quote.copy( - diffPercent = DifferencePercent.Best, - isSingleProvider = isSingleProvider, - ) - } else { - // current / selected - 1 - val percent = quote.quoteAmount / bestQuote.quoteAmount - BigDecimal.ONE - quote.copy( - diffPercent = DifferencePercent.Diff( - isPositive = percent.isPositive(), - percent = stringReference( - if (percent.isPositive()) { - "${StringsSigns.PLUS}${percent.format { percent() }}" - } else { - "${StringsSigns.DASH_SIGN}${percent.format { percent() }}" - }, - ), - ), - ) - } + return sortedQuotes.map { quote -> + if (quote is SwapQuoteUM.Content && bestQuote is SwapQuoteUM.Content) { + if (quote.provider.providerId == bestQuote.provider.providerId) { + quote.copy( + diffPercent = DifferencePercent.Best, + isSingleProvider = isSingleProvider, + ) } else { - quote + val percent = if (selectedAmountType == SwapAmountType.To) { + // Fixed mode: best has lowest fromTokenAmount; compare as (best/current - 1) + val bestFrom = bestQuote.fromAmount + val quoteFrom = quote.fromAmount + if (bestFrom != null && quoteFrom != null && quoteFrom > BigDecimal.ZERO) { + bestFrom / quoteFrom - BigDecimal.ONE + } else { + BigDecimal.ZERO + } + } else { + // Float mode: best has highest toAmount; compare as (current/best - 1) + quote.toAmount / bestQuote.toAmount - BigDecimal.ONE + } + quote.copy( + diffPercent = DifferencePercent.Diff( + isPositive = percent.isPositive(), + percent = stringReference( + if (percent.isPositive()) { + "${StringsSigns.PLUS}${percent.format { percent() }}" + } else { + "${StringsSigns.DASH_SIGN}${percent.format { percent() }}" + }, + ), + ), + ) } - }.toPersistentList() + } else { + quote + } + }.toPersistentList() } - private fun findBestQuote(quotes: List): SwapQuoteUM? { - return quotes - .sortedWith(SwapQuotesComparator) - .firstOrNull() + private fun findBestQuote(quotes: List, comparator: Comparator): SwapQuoteUM? { + return quotes.sortedWith(comparator).firstOrNull() } - private object SwapQuotesComparator : Comparator { + private class SwapQuotesComparator(private val selectedAmountType: SwapAmountType) : Comparator { override fun compare(p0: SwapQuoteUM?, p1: SwapQuoteUM?): Int { return when { p0 is SwapQuoteUM.Content && p1 !is SwapQuoteUM.Content -> -1 p0 !is SwapQuoteUM.Content && p1 is SwapQuoteUM.Content -> 1 p0 is SwapQuoteUM.Error && p1 is SwapQuoteUM.Error -> compareErrorQuote(p0 = p0, p1 = p1) - p0 is SwapQuoteUM.Content && p1 is SwapQuoteUM.Content -> p1.quoteAmount.compareTo(p0.quoteAmount) + p0 is SwapQuoteUM.Content && p1 is SwapQuoteUM.Content -> compareContent(p0, p1) else -> 0 } } + private fun compareContent(p0: SwapQuoteUM.Content, p1: SwapQuoteUM.Content): Int { + return if (selectedAmountType == SwapAmountType.To) { + // Fixed mode: lower fromTokenAmount = better (ascending) + val f0 = p0.fromAmount ?: BigDecimal.ZERO + val f1 = p1.fromAmount ?: BigDecimal.ZERO + f0.compareTo(f1) + } else { + // Float mode: higher toAmount = better (descending) + p1.toAmount.compareTo(p0.toAmount) + } + } + private fun compareErrorQuote(p0: SwapQuoteUM.Error, p1: SwapQuoteUM.Error): Int = when { p0.expressError is ExpressError.AmountError && p1.expressError !is ExpressError.AmountError -> -1 p0.expressError !is ExpressError.AmountError && p1.expressError is ExpressError.AmountError -> 1 diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index b4df59fefc..d75eb733a2 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -37,9 +37,9 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt index b314e610b9..69d9cb7e19 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountContent.kt @@ -4,6 +4,7 @@ import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* @@ -39,9 +40,9 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.express.models.ExpressRateType -import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountClickIntents import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountClickIntentsStub @@ -156,10 +157,12 @@ private fun SwapAmountBlock( Box { SwapAmountEditBlock( amountFieldUM = amountFieldUM, + isFixedRate = isFixedRate, modifier = Modifier, onValueChange = clickIntents::onAmountValueChange, onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, onCurrencyChange = clickIntents::onCurrencyChangeClick, + onRateClick = clickIntents::onRateClick, ) HorizontalDivider( modifier = Modifier @@ -182,12 +185,15 @@ private fun SwapAmountBlock( } } +@Suppress("LongParameterList") @Composable private fun SwapAmountEditBlock( amountFieldUM: SwapAmountFieldUM, + isFixedRate: Boolean, onValueChange: (String) -> Unit, onValuePastedTriggerDismiss: () -> Unit, onCurrencyChange: (Boolean) -> Unit, + onRateClick: () -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -209,8 +215,44 @@ private fun SwapAmountEditBlock( onValueChange = onValueChange, onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, onCurrencyChange = onCurrencyChange, + reserveSpaceForError = false, modifier = Modifier, ) + SwapRateBadge( + isFixedRate = isFixedRate, + onClick = onRateClick, + ) + } +} + +@Composable +private fun SwapRateBadge(isFixedRate: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val iconRes = if (isFixedRate) R.drawable.ic_fixed else R.drawable.ic_floating + val textRes = if (isFixedRate) R.string.send_rate_is_fixed else R.string.send_rate_floating_info_title + Row( + modifier = modifier + .clip(RoundedCornerShape(6.dp)) + .clickable(onClick = onClick) + .border( + width = 1.dp, + color = TangemTheme.colors.stroke.primary, + shape = RoundedCornerShape(6.dp), + ) + .padding(vertical = 2.dp, horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + painter = rememberVectorPainter(ImageVector.vectorResource(id = iconRes)), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing2), + ) + Text( + text = stringResourceSafe(textRes), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) } } @@ -227,6 +269,7 @@ private fun SwapAmountInfo( modifier: Modifier = Modifier, ) { val tokenIconState = (amountFieldUM.amountField as? AmountState.Data)?.tokenIconState ?: CurrencyIconState.Loading + val isEnabled = (amountFieldUM as? SwapAmountFieldUM.Content)?.isClickEnabled == true Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), @@ -234,11 +277,11 @@ private fun SwapAmountInfo( .clickable( interactionSource = remember { MutableInteractionSource() }, indication = ripple(), - enabled = (amountFieldUM as? SwapAmountFieldUM.Content)?.isClickEnabled == true, + enabled = isEnabled, onClick = { - if (isFixedRate) { + if (!isSelectedAmountType) { onExpandEditField() - } else { + } else if (amountFieldUM.amountType == SwapAmountType.To) { onSelectTokenClick() } }, @@ -256,15 +299,14 @@ private fun SwapAmountInfo( SwapAmountInfoMain( amountFieldUM = amountFieldUM, selectedQuote = selectedQuote, - isSelectedAmountType = isSelectedAmountType, modifier = Modifier.weight(1f), ) AnimatedContent( targetState = isSelectedAmountType, ) { isSelected -> - if (isSelected) { + if (isSelected && amountFieldUM.amountType == SwapAmountType.From) { AmountMaxButton(onMaxAmountClick) - } else { + } else if (isSelected && amountFieldUM.amountType == SwapAmountType.To) { SwapAmountInfoQuote( quoteUM = selectedQuote, isFixedRate = isFixedRate, @@ -279,7 +321,6 @@ private fun SwapAmountInfo( private fun SwapAmountInfoMain( amountFieldUM: SwapAmountFieldUM, selectedQuote: SwapQuoteUM?, - isSelectedAmountType: Boolean, modifier: Modifier = Modifier, ) { Column( @@ -300,48 +341,52 @@ private fun SwapAmountInfoMain( ) } } - AnimatedContent(isSelectedAmountType) { isSelected -> - if (isSelected && amountFieldUM is SwapAmountFieldUM.Content) { - SpacerH2() - Row { - EllipsisText( - text = amountFieldUM.subtitleLeft.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ellipsis = amountFieldUM.subtitleEllipsisLeft, - modifier = Modifier.weight(1f, fill = false), - ) - EllipsisText( - text = amountFieldUM.subtitleRight.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ellipsis = amountFieldUM.subtitleEllipsisRight, - ) - } - } else { - AnimatedContent(selectedQuote) { quote -> - when (quote) { - is SwapQuoteUM.Content -> { - SpacerH2() + + AnimatedContent(selectedQuote) { quote -> + when (quote) { + is SwapQuoteUM.Content -> { + SpacerH2() + if (amountFieldUM is SwapAmountFieldUM.Content) { + SpacerH2() + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { EllipsisText( - text = stringResourceSafe( - R.string.send_with_swap_recipient_get_amount, - quote.quoteAmountValue.resolveReference(), - ), + text = amountFieldUM.subtitleLeft.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, + ellipsis = amountFieldUM.subtitleEllipsisLeft, + modifier = Modifier.weight(1f, fill = false), ) - } - SwapQuoteUM.Loading -> { - SpacerH2() - TextShimmer( + EllipsisText( + text = amountFieldUM.subtitleRight.resolveReference(), style = TangemTheme.typography.caption2, - modifier = Modifier.width(72.dp), + color = TangemTheme.colors.text.tertiary, + ellipsis = amountFieldUM.subtitleEllipsisRight, ) } - else -> Unit + } else { + SpacerH2() + TextShimmer( + style = TangemTheme.typography.caption2, + modifier = Modifier.width(72.dp), + ) } } + SwapQuoteUM.Loading -> { + SpacerH2() + TextShimmer( + style = TangemTheme.typography.caption2, + modifier = Modifier.width(72.dp), + ) + } + else -> { + SpacerH2() + Text( + text = "", + style = TangemTheme.typography.caption2, + ) + } } } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt index 4a7380480b..351e841fba 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountClickIntentsStub.kt @@ -10,6 +10,8 @@ internal object SwapAmountClickIntentsStub : SwapAmountClickIntents { override fun onSelectTokenClick() {} + override fun onRateClick() {} + override fun onSeparatorClick() {} override fun onProviderClick() {} diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index b4ebcff7cb..6f354c847a 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -12,10 +12,11 @@ import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection +import com.tangem.domain.swap.models.SwapRateMode import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM +import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.utils.StringsSigns @@ -66,8 +67,10 @@ internal data object SwapAmountContentPreview { private val quote = SwapQuoteUM.Content( provider = provider, - quoteAmount = "123".toBigDecimal(), - quoteAmountValue = stringReference("123"), + toAmount = "123".toBigDecimal(), + fromAmount = null, + toAmountValue = stringReference("123"), + fromAmountValue = TextReference.EMPTY, rate = stringReference("1 USD ≈ 123.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, isSingleProvider = false, @@ -92,6 +95,7 @@ internal data object SwapAmountContentPreview { appCurrency = AppCurrency.Default, isShowBestRateAnimation = false, isShowFCAWarning = false, + swapRateMode = SwapRateMode.FLOAT_ONLY, ) val defaultState = SwapAmountUM.Content( @@ -127,10 +131,11 @@ internal data object SwapAmountContentPreview { selectedQuote = quote, primaryCryptoCurrencyStatus = cryptoCurrencyStatus, secondaryCryptoCurrencyStatus = cryptoCurrencyStatus, - swapRateType = ExpressRateType.Float, + swapRateType = ExpressRateType.Fixed, isPrimaryButtonEnabled = true, isShowBestRateAnimation = false, isShowFCAWarning = true, + swapRateMode = SwapRateMode.FLOAT_AND_FIXED, ) val defaultStateAccount: SwapAmountUM.Content diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt index 56c7a0f93a..7383f568e4 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderListItemConverter.kt @@ -41,7 +41,7 @@ internal class SwapProviderListItemConverter( title = stringReference(provider.name), subtitle = stringReference(provider.type.typeName), infoText = when (value) { - is SwapQuoteUM.Content -> value.quoteAmountValue + is SwapQuoteUM.Content -> value.toAmountValue else -> TextReference.EMPTY }, iconUrl = provider.imageLarge, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt index 54f3030f4f..a591206770 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/converter/SwapProviderStateConverter.kt @@ -50,7 +50,7 @@ internal class SwapProviderStateConverter( name = provider.name, iconUrl = provider.imageLarge, type = provider.type.typeName, - subtitle = quoteAmountValue, + subtitle = toAmountValue, additionalBadge = additionalBadge, diffPercent = diffPercent, isSelected = provider == selectedProvider, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt index 238cf49cb3..4765401f15 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt @@ -35,8 +35,10 @@ internal object SwapChooseProviderContentPreview { private val quote1 = SwapQuoteUM.Content( provider = provider1, - quoteAmount = "123".toBigDecimal(), - quoteAmountValue = stringReference("123"), + toAmount = "123".toBigDecimal(), + fromAmount = null, + toAmountValue = stringReference("123"), + fromAmountValue = stringReference(""), rate = stringReference("1 USD ≈ 123.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Best, isSingleProvider = false, @@ -44,8 +46,10 @@ internal object SwapChooseProviderContentPreview { private val quote2 = SwapQuoteUM.Content( provider = provider2, - quoteAmount = "13.12".toBigDecimal(), - quoteAmountValue = stringReference("13.12"), + toAmount = "13.12".toBigDecimal(), + fromAmount = null, + toAmountValue = stringReference("13.12"), + fromAmountValue = stringReference(""), rate = stringReference("1 USD ≈ 12.123 POL"), diffPercent = SwapQuoteUM.Content.DifferencePercent.Empty, isSingleProvider = false, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt index b4466b11a5..9a6c3072c5 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/ui/SwapChooseTokenNetworkContent.kt @@ -6,11 +6,9 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.Image -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.material3.ripple @@ -25,7 +23,6 @@ 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.SpacerWMax import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.message.* import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet @@ -141,21 +138,6 @@ private fun SwapChooseTokenNetworkContentList(swapNetworks: ImmutableList Unit): TextReference { +private fun SendWithSwapFooter( + confirmUM: ConfirmUM, + stackState: ChildStack, + primaryButton: NavigationButton, + onLinkClick: (String) -> Unit, +) { + Column { + AnimatedVisibility( + visible = stackState.active.configuration == SendWithSwapRoute.Confirm, + enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(), + ) { + val confirmContentUM = confirmUM as? ConfirmUM.Content + val sendFooter = confirmContentUM?.sendingFooter ?: TextReference.EMPTY + val legalFooter = getAnnotatedStringForLegals( + tosUM = confirmContentUM?.tosUM, + sendFooter = sendFooter, + onClick = onLinkClick, + ) + val footerText = remember(sendFooter, legalFooter) { + if (sendFooter != TextReference.EMPTY || legalFooter != TextReference.EMPTY) { + combinedReference(sendFooter, legalFooter) + } else { + TextReference.EMPTY + } + } + SendingText(footerText = footerText) + } + NavigationPrimaryButton( + primaryButton = primaryButton, + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) + } +} + +@Composable +private fun getAnnotatedStringForLegals( + tosUM: ConfirmUM.Content.TosUM?, + sendFooter: TextReference, + onClick: (String) -> Unit, +): TextReference { if (tosUM == null) return TextReference.EMPTY val tos = tosUM.tosLink val policy = tosUM.policyLink @@ -118,7 +144,9 @@ private fun getAnnotatedStringForLegals(tosUM: ConfirmUM.Content.TosUM?, onClick val policyIndex = fullString.indexOf(policyTitle) annotatedReference { - append(StringsSigns.POINT_SIGN) + if (!sendFooter.resolveReference().endsWith(StringsSigns.POINT_SIGN)) { + append(StringsSigns.POINT_SIGN) + } appendSpace() append(fullString.substring(0, tosIndex)) withLink( From 35f4f97408d1325dca93b2ac4306d58ba2173fb4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 23 Mar 2026 16:08:03 +0400 Subject: [PATCH 07/75] Updated on 2026-08-14 --- .../com/tangem/tap/common/DialogManager.kt | 19 ----- .../com/tangem/tap/common/redux/AppDialog.kt | 9 --- .../com/tangem/tap/common/redux/AppState.kt | 2 - .../products/wallet/redux/BackupDialog.kt | 14 ---- .../products/wallet/redux/BackupMiddleware.kt | 63 --------------- .../wallet/redux/OnboardingWalletAction.kt | 19 ----- .../dialogs/ConfirmDiscardingBackupDialog.kt | 29 ------- .../ui/dialogs/UnfinishedBackupFoundDialog.kt | 33 -------- .../component/impl/DefaultRoutingComponent.kt | 77 ++++++++++++++++++- 9 files changed, 73 insertions(+), 192 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index c863320c2a..c3a367a894 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -7,12 +7,8 @@ import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.ui.ScanFailsDialog import com.tangem.tap.common.ui.SimpleAlertDialog -import com.tangem.tap.common.ui.SimpleCancelableAlertDialog import com.tangem.tap.common.ui.SimpleOkDialog import com.tangem.tap.features.onboarding.OnboardingDialog -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog -import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog -import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletActivationErrorDialog import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletAlreadyWasUsedDialog import com.tangem.tap.store @@ -60,14 +56,6 @@ class DialogManager : StoreSubscriber { context = context, ) is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog) - is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create( - context = context, - scanResponse = state.dialog.scanResponse, - ) - is BackupDialog.ConfirmDiscardingBackup -> ConfirmDiscardingBackupDialog.create( - context = context, - unfinishedBackupScanResponse = state.dialog.scanResponse, - ) is AppDialog.TokensAreLinkedDialog -> SimpleAlertDialog.create( title = context.getString(state.dialog.titleRes, state.dialog.currencySymbol), message = context.getString( @@ -84,13 +72,6 @@ class DialogManager : StoreSubscriber { onSupport = state.dialog.onSupportClick, onCancel = state.dialog.onCancel, ) - is AppDialog.RemoveWalletDialog -> SimpleCancelableAlertDialog.create( - title = context.getString(state.dialog.titleRes, state.dialog.currencyTitle), - messageRes = state.dialog.messageRes, - context = context, - primaryButtonRes = state.dialog.primaryButtonRes, - primaryButtonAction = state.dialog.onOk, - ) else -> null } dialog?.show() diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt b/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt index 8c9f98ebdd..69ef67c88a 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt @@ -15,15 +15,6 @@ sealed class AppDialog : StateDialog { val onOk: VoidCallback? = null, ) : AppDialog() - data class RemoveWalletDialog( - val currencyTitle: String, - val onOk: () -> Unit, - ) : AppDialog() { - val messageRes: Int = R.string.token_details_hide_alert_message - val titleRes: Int = R.string.token_details_hide_alert_title - val primaryButtonRes: Int = R.string.token_details_hide_alert_hide - } - data class TokensAreLinkedDialog( val currencyTitle: String, val currencySymbol: String, diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index ae347d0807..89851458ac 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -5,7 +5,6 @@ import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.redux.legacy.LegacyMiddleware import com.tangem.tap.features.details.redux.DetailsMiddleware import com.tangem.tap.features.details.redux.DetailsState -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware import com.tangem.tap.proxy.redux.DaggerGraphMiddleware import com.tangem.tap.proxy.redux.DaggerGraphState import org.rekotlin.Middleware @@ -23,7 +22,6 @@ data class AppState( logMiddleware, GlobalMiddleware.handler, DetailsMiddleware().detailsMiddleware, - BackupMiddleware().backupMiddleware, LockUserWalletsTimerMiddleware().middleware, AccessCodeRequestPolicyMiddleware().middleware, DaggerGraphMiddleware.daggerGraphMiddleware, diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupDialog.kt deleted file mode 100644 index ef465fad15..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupDialog.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.redux - -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.redux.StateDialog - -sealed class BackupDialog : StateDialog { - data class UnfinishedBackupFound( - val scanResponse: ScanResponse? = null, - ) : BackupDialog() - - data class ConfirmDiscardingBackup( - val scanResponse: ScanResponse? = null, - ) : BackupDialog() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt deleted file mode 100644 index e0e8bd6b12..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.redux - -import com.tangem.common.routing.AppRoute -import com.tangem.core.analytics.Analytics -import com.tangem.tap.backupService -import com.tangem.tap.common.analytics.events.Onboarding.Finished -import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.demo.DemoHelper -import com.tangem.tap.mainScope -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.store -import kotlinx.coroutines.launch -import org.rekotlin.Middleware - -@Suppress("MemberNameEqualsClassName") -class BackupMiddleware { - val backupMiddleware: Middleware = { dispatch, state -> - { next -> - { action -> - if (action is BackupAction) handleBackupAction(state, action) - next(action) - } - } - } -} - -@Suppress("LongMethod", "ComplexMethod", "MagicNumber") -private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) { - if (DemoHelper.tryHandle(appState)) return - - when (action) { - is BackupAction.DiscardBackup -> { - backupService.discardSavedBackup() - } - is BackupAction.DiscardSavedBackup -> { - mainScope.launch { - backupService.discardSavedBackup() - - val onboardingRepository = store.inject(DaggerGraphState::onboardingRepository) - val cardRepository = store.inject(DaggerGraphState::cardRepository) - val unfinishedBackup = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch - - cardRepository.finishCardActivation(unfinishedBackup.card.cardId) - onboardingRepository.clearUnfinishedFinalizeOnboarding() - Analytics.send(Finished()) - } - } - is BackupAction.ResumeFoundUnfinishedBackup -> { - if (action.unfinishedBackupScanResponse != null) { - store.dispatchNavigationAction { - replaceAll( - AppRoute.Onboarding( - scanResponse = action.unfinishedBackupScanResponse, - mode = AppRoute.Onboarding.Mode.ContinueFinalize, - ), - ) - } - } - } - } -} \ No newline at end of file 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 deleted file mode 100644 index db39ad8c56..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.redux - -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.models.wallet.UserWalletId -import org.rekotlin.Action - -sealed class OnboardingWalletAction : Action { - data class WalletSaved(val userWalletId: UserWalletId) : OnboardingWalletAction() -} - -sealed class BackupAction : Action { - - data object DiscardBackup : BackupAction() - data object DiscardSavedBackup : BackupAction() - - data class ResumeFoundUnfinishedBackup( - val unfinishedBackupScanResponse: ScanResponse?, - ) : BackupAction() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt deleted file mode 100644 index 9f5257bd04..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction -import com.tangem.tap.store -import com.tangem.wallet.R - -object ConfirmDiscardingBackupDialog { - fun create(context: Context, unfinishedBackupScanResponse: ScanResponse? = null): AlertDialog { - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(R.string.welcome_interrupted_backup_discard_title) - setMessage(R.string.welcome_interrupted_backup_discard_message) - setPositiveButton(R.string.welcome_interrupted_backup_discard_resume) { _, _ -> - store.dispatch(BackupAction.ResumeFoundUnfinishedBackup(unfinishedBackupScanResponse)) - } - setNegativeButton(R.string.welcome_interrupted_backup_discard_discard) { _, _ -> - store.dispatch(BackupAction.DiscardSavedBackup) - } - setOnDismissListener { - store.dispatch(GlobalAction.HideDialog) - } - setCancelable(false) - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt deleted file mode 100644 index df8cc61880..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.core.analytics.Analytics -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog -import com.tangem.tap.store -import com.tangem.wallet.R - -object UnfinishedBackupFoundDialog { - fun create(context: Context, scanResponse: ScanResponse? = null): AlertDialog { - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(R.string.common_warning) - setMessage(R.string.welcome_interrupted_backup_alert_message) - setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ -> - Analytics.send(OnboardingEvent.Backup.ResumeInterruptedBackup()) - store.dispatch(GlobalAction.HideDialog) - store.dispatch(BackupAction.ResumeFoundUnfinishedBackup(scanResponse)) - } - setNegativeButton(R.string.welcome_interrupted_backup_alert_discard) { _, _ -> - Analytics.send(OnboardingEvent.Backup.CancelInterruptedBackup()) - store.dispatch(GlobalAction.HideDialog) - store.dispatch(GlobalAction.ShowDialog(BackupDialog.ConfirmDiscardingBackup(scanResponse))) - } - setCancelable(false) - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index a4cdba0883..fb71667669 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -23,20 +23,25 @@ import com.tangem.core.decompose.navigation.getOrCreateTyped import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.features.hotwallet.HotAccessCodeRequestComponent import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy +import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.walletconnect.components.WcRoutingComponent import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.android.create +import com.tangem.sdk.api.BackupServiceHolder import com.tangem.tap.common.SnackbarHandler -import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.analytics.events.Onboarding +import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.hot.TangemHotSDKProxy -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog import com.tangem.tap.features.root.RootDetectedWarningComponent import com.tangem.tap.routing.RootContent import com.tangem.tap.routing.component.RoutingComponent @@ -46,6 +51,7 @@ import com.tangem.tap.routing.utils.ChildFactory import com.tangem.tap.routing.utils.DeepLinkFactory import com.tangem.tap.store import com.tangem.utils.logging.TangemLogger +import com.tangem.wallet.R import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -71,6 +77,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsExceptionHandler: AnalyticsExceptionHandler, + private val backupServiceHolder: BackupServiceHolder, ) : RoutingComponent, AppComponentContext by context, SnackbarHandler { @@ -251,9 +258,71 @@ internal class DefaultRoutingComponent @AssistedInject constructor( } private fun checkForUnfinishedBackup() { + if (DemoHelper.tryHandle { store.state }) return componentScope.launch(dispatchers.main) { - val onboardingScanResponse = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch - store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound(onboardingScanResponse))) + val scanResponse = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch + messageSender.send(unfinishedBackupFoundDialog(scanResponse)) + } + } + + private fun unfinishedBackupFoundDialog(scanResponse: ScanResponse): DialogMessage = DialogMessage( + title = resourceReference(R.string.common_warning), + message = resourceReference(R.string.welcome_interrupted_backup_alert_message), + isDismissable = false, + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.welcome_interrupted_backup_alert_resume), + onClick = { + analyticsEventHandler.send(OnboardingEvent.Backup.ResumeInterruptedBackup()) + resumeUnfinishedBackup(scanResponse) + }, + ) + }, + secondActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.welcome_interrupted_backup_alert_discard), + onClick = { + analyticsEventHandler.send(OnboardingEvent.Backup.CancelInterruptedBackup()) + messageSender.send(confirmDiscardingBackupDialog(scanResponse)) + }, + ) + }, + ) + + private fun confirmDiscardingBackupDialog(scanResponse: ScanResponse): DialogMessage = DialogMessage( + title = resourceReference(R.string.welcome_interrupted_backup_discard_title), + message = resourceReference(R.string.welcome_interrupted_backup_discard_message), + isDismissable = false, + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.welcome_interrupted_backup_discard_resume), + onClick = { resumeUnfinishedBackup(scanResponse) }, + ) + }, + secondActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.welcome_interrupted_backup_discard_discard), + onClick = { discardSavedBackup() }, + ) + }, + ) + + private fun resumeUnfinishedBackup(scanResponse: ScanResponse) { + router.replaceAll( + AppRoute.Onboarding( + scanResponse = scanResponse, + mode = AppRoute.Onboarding.Mode.ContinueFinalize, + ), + ) + } + + private fun discardSavedBackup() { + componentScope.launch(dispatchers.main) { + backupServiceHolder.backupService.get()?.discardSavedBackup() + val unfinishedBackup = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch + cardRepository.finishCardActivation(unfinishedBackup.card.cardId) + onboardingRepository.clearUnfinishedFinalizeOnboarding() + analyticsEventHandler.send(Onboarding.Finished()) } } From 8922d23eec0348c0ed974a18111360324f93a80f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Mar 2026 07:27:41 +0100 Subject: [PATCH 08/75] Updated on 2026-08-14 --- .../tangem/core/ui/ds/topbar/TangemTopBar.kt | 50 ++++--- .../components/DefaultFeedEntryComponent.kt | 7 +- .../components/earn/DefaultEarnComponent.kt | 57 ++++++-- .../DefaultMarketsTokenDetailsComponent.kt | 76 ++++++++-- .../details/DefaultNewsDetailsComponent.kt | 2 - .../features/feed/model/earn/EarnModel.kt | 1 + .../model/earn/state/EarnStateController.kt | 6 + .../UpdateEarnUMInitialStateTransformer.kt | 8 ++ .../feed/model/feed/FeedComponentModel.kt | 8 +- .../details/MarketsTokenDetailsModel.kt | 5 + .../feed/ui/components/FeedSearchBar.kt | 136 ++++++++++++++++++ .../features/feed/ui/earn/EarnContent.kt | 6 + .../features/feed/ui/earn/state/EarnUM.kt | 2 + .../tangem/features/feed/ui/feed/FeedList.kt | 51 +------ .../feed/ui/feed/components/BlockHeader.kt | 6 +- .../feed/ui/feed/components/DateBlock.kt | 32 +++++ .../feed/ui/feed/components/MarketsBlock.kt | 4 +- .../preview/MarketsTokenDetailsPreview.kt | 10 ++ .../detailed/state/MarketsTokenDetailsUM.kt | 2 + 19 files changed, 374 insertions(+), 95 deletions(-) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/FeedSearchBar.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt index 9b687679f5..c05ffb7daa 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt @@ -102,6 +102,7 @@ fun TangemTopBar( endContent = endContent, content = { Column( + modifier = Modifier.weight(1f), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5), ) { @@ -128,47 +129,54 @@ fun TangemTopBar( * A top bar composable that displays a title and optional start and end icons. * [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8435-74860&m=dev) * - * @param modifier Modifier to be applied to the top bar. - * @param type Type of the top bar, which determines its size and padding. - * @param content Composable content to be displayed in the center of the top bar - * @param startContent Optional composable content to be displayed at the start (left) of the top bar. - * @param endContent Optional composable content to be displayed at the end (right) of the top bar. + * @param modifier Modifier to be applied to the top bar. + * @param type Type of the top bar, which determines its size and padding. + * @param content Composable content to be displayed in the center of the top bar + * @param startContent Optional composable content to be displayed at the start (left) of the top bar. + * @param endContent Optional composable content to be displayed at the end (right) of the top bar. + * @param reserveSlotSpace If true (default), slot space is always reserved even when start/end content is null, + * ensuring centered alignment of [content]. Set to false for edge-to-edge content like + * search fields, where empty slots should not consume space. */ @Composable fun TangemTopBar( modifier: Modifier = Modifier, type: TangemTopBarType = TangemTopBarType.Default, - content: @Composable () -> Unit, + reserveSlotSpace: Boolean = true, + content: @Composable RowScope.() -> Unit, startContent: @Composable (() -> Unit)? = null, endContent: @Composable (() -> Unit)? = null, ) { Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, modifier = modifier .fillMaxWidth() .heightIn(min = type.getSize()) .padding(type.getPadding()), ) { - AnimatedContent( - targetState = startContent != null, - modifier = Modifier.size(TangemTheme.dimens2.x11), - label = "Start Content Visibility", - ) { isVisible -> - if (isVisible) { - startContent?.invoke() + if (reserveSlotSpace || startContent != null) { + AnimatedContent( + targetState = startContent != null, + modifier = Modifier.size(TangemTheme.dimens2.x11), + label = "Start Content Visibility", + ) { isVisible -> + if (isVisible) { + startContent?.invoke() + } } } content() - AnimatedContent( - targetState = endContent != null, - modifier = Modifier.size(TangemTheme.dimens2.x11), - label = "End Content Visibility", - ) { isVisible -> - if (isVisible) { - endContent?.invoke() + if (reserveSlotSpace || endContent != null) { + AnimatedContent( + targetState = endContent != null, + modifier = Modifier.size(TangemTheme.dimens2.x11), + label = "End Content Visibility", + ) { isVisible -> + if (isVisible) { + endContent?.invoke() + } } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt index 9b8a539048..9538938a8c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt @@ -78,6 +78,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( screenSource = AnalyticsParam.ScreensSources.Token, ) }, + onMarketOpenClick = { onMarketOpenClick(null) }, ), ), ) @@ -136,7 +137,10 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( override fun onOpenEarnPage() { innerRouter.push( FeedEntryChildFactory.Child.Earn( - params = DefaultEarnComponent.Params(onBackClick = { onChildBack() }), + params = DefaultEarnComponent.Params( + onBackClick = { onChildBack() }, + onMarketOpenClick = { onMarketOpenClick(null) }, + ), ), ) } @@ -240,6 +244,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( paginationConfig = null, ) }, + onMarketOpenClick = { clickIntents.onMarketOpenClick(null) }, ), ) FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt index a0b7dcb1e8..e5915d7103 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt @@ -1,9 +1,17 @@ package com.tangem.features.feed.components.earn +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +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.State import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState @@ -17,11 +25,15 @@ import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent +import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme import com.tangem.features.feed.components.feed.FeedBottomSheetRoute import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.model.earn.EarnModel +import com.tangem.features.feed.ui.components.FeedSearchBar import com.tangem.features.feed.ui.earn.EarnContent import kotlinx.serialization.Serializable @@ -44,15 +56,41 @@ internal class DefaultEarnComponent( override fun Title(bottomSheetState: State) { val background = LocalMainBottomSheetColor.current.value val state by earnModel.state.collectAsStateWithLifecycle() - TangemTopAppBar( - containerColor = background, - title = stringResourceSafe(R.string.earn_title), - startButton = TopAppBarButtonUM.Icon( - iconRes = R.drawable.ic_back_24, - onClicked = state.onBackClick, - isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED, - ), - ) + if (LocalRedesignEnabled.current) { + FeedSearchBar( + isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED, + feedListSearchBar = state.feedListSearchBar, + modifier = Modifier.drawBehind { drawRect(background) }, + startContent = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.primary, + modifier = Modifier + .size(TangemTheme.dimens2.x11) + .background( + color = TangemTheme.colors2.button.backgroundSecondary, + shape = CircleShape, + ) + .clickableSingle( + onClick = state.onBackClick, + enabled = bottomSheetState.value == BottomSheetState.EXPANDED, + ) + .padding(TangemTheme.dimens2.x2), + ) + }, + ) + } else { + TangemTopAppBar( + containerColor = background, + title = stringResourceSafe(R.string.earn_title), + startButton = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_back_24, + onClicked = state.onBackClick, + isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED, + ), + ) + } } @Composable @@ -94,5 +132,6 @@ internal class DefaultEarnComponent( @Serializable data class Params( val onBackClick: () -> Unit, + val onMarketOpenClick: () -> Unit, ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt index d43ace2233..93292cc527 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt @@ -1,10 +1,18 @@ package com.tangem.features.feed.components.market.details +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +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.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network @@ -12,9 +20,13 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent +import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.currency.CryptoCurrency @@ -22,6 +34,7 @@ import com.tangem.features.feed.components.market.details.portfolio.api.MarketsP import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent import com.tangem.features.feed.model.market.details.state.TokenNetworksState +import com.tangem.features.feed.ui.components.FeedSearchBar import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsContent import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsTopBar import kotlinx.coroutines.flow.collectLatest @@ -86,15 +99,59 @@ internal class DefaultMarketsTokenDetailsComponent( override fun Title(bottomSheetState: State) { val state by model.state.collectAsStateWithLifecycle() val background = LocalMainBottomSheetColor.current.value - MarketsTokenDetailsTopBar( - onBackClick = { params.onBackClicked() }, - isBackButtonEnabled = bottomSheetState.value == BottomSheetState.EXPANDED, - shouldShowPriceSubtitle = state.shouldShowPriceSubtitle, - tokenName = state.tokenName, - tokenPrice = state.priceText, - backgroundColor = background, - onShareClick = state.onShareClick, - ) + if (LocalRedesignEnabled.current) { + FeedSearchBar( + isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED, + feedListSearchBar = state.feedListSearchBar, + modifier = Modifier.drawBehind { drawRect(background) }, + startContent = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.primary, + modifier = Modifier + .size(TangemTheme.dimens2.x11) + .background( + color = TangemTheme.colors2.button.backgroundSecondary, + shape = CircleShape, + ) + .clickableSingle( + onClick = { params.onBackClicked() }, + enabled = bottomSheetState.value == BottomSheetState.EXPANDED, + ) + .padding(TangemTheme.dimens2.x2), + ) + }, + endContent = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_share_new_24), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.primary, + modifier = Modifier + .size(TangemTheme.dimens2.x11) + .background( + color = TangemTheme.colors2.button.backgroundSecondary, + shape = CircleShape, + ) + .clickableSingle( + onClick = state.onShareClick, + enabled = bottomSheetState.value == BottomSheetState.EXPANDED, + ) + .padding(TangemTheme.dimens2.x2_5), + ) + }, + ) + } else { + MarketsTokenDetailsTopBar( + onBackClick = { params.onBackClicked() }, + isBackButtonEnabled = bottomSheetState.value == BottomSheetState.EXPANDED, + shouldShowPriceSubtitle = state.shouldShowPriceSubtitle, + tokenName = state.tokenName, + tokenPrice = state.priceText, + backgroundColor = background, + onShareClick = state.onShareClick, + ) + } } @Composable @@ -131,6 +188,7 @@ internal class DefaultMarketsTokenDetailsComponent( val analyticsParams: AnalyticsParams?, val onBackClicked: () -> Unit, val onArticleClick: (articleId: Int, preselectedArticlesId: List) -> Unit, + val onMarketOpenClick: () -> Unit, ) @Serializable diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt index 1d31f11fe9..7b0b6465db 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt @@ -22,7 +22,6 @@ import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.ds.topbar.TangemTopBar import com.tangem.core.ui.ds.topbar.TangemTopBarType import com.tangem.core.ui.extensions.clickableSingle -import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme @@ -47,7 +46,6 @@ internal class DefaultNewsDetailsComponent( val state by newsDetailsModel.state.collectAsStateWithLifecycle() if (LocalRedesignEnabled.current) { TangemTopBar( - title = resourceReference(R.string.common_news), type = TangemTopBarType.BottomSheet, startContent = { Icon( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index 8622c63f4b..71a2894d6b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -325,6 +325,7 @@ internal class EarnModel @Inject constructor( onNetworkFilterClick = ::onNetworkFilterClick, onTypeFilterClick = ::onTypeFilterClick, onScroll = ::onMostlyUsedScrolled, + onSearchBarClicked = params.onMarketOpenClick, ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt index f074cbdc66..34a71d62a2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt @@ -1,8 +1,10 @@ package com.tangem.features.feed.model.earn.state import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.extensions.TextReference import com.tangem.features.feed.model.earn.state.transformers.EarnUMTransformer import com.tangem.features.feed.ui.earn.state.* +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -34,6 +36,10 @@ internal class EarnStateController @Inject constructor() { onNetworkFilterClick = {}, onTypeFilterClick = {}, onSliderScroll = {}, + feedListSearchBar = FeedListSearchBar( + placeholderText = TextReference.EMPTY, + onBarClick = {}, + ), ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt index d46ac02956..8991f72503 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt @@ -1,12 +1,16 @@ package com.tangem.features.feed.model.earn.state.transformers +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.feed.impl.R import com.tangem.features.feed.ui.earn.state.EarnUM +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar internal class UpdateEarnUMInitialStateTransformer( private val onBackClick: () -> Unit, private val onNetworkFilterClick: () -> Unit, private val onTypeFilterClick: () -> Unit, private val onScroll: () -> Unit, + private val onSearchBarClicked: () -> Unit, ) : EarnUMTransformer { override fun transform(prevState: EarnUM): EarnUM { @@ -15,6 +19,10 @@ internal class UpdateEarnUMInitialStateTransformer( onNetworkFilterClick = onNetworkFilterClick, onTypeFilterClick = onTypeFilterClick, onSliderScroll = onScroll, + feedListSearchBar = FeedListSearchBar( + onBarClick = onSearchBarClicked, + placeholderText = resourceReference(id = R.string.markets_search_title_placeholder), + ), ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt index 9fb42bb041..3252a220e0 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -232,7 +232,13 @@ internal class FeedComponentModel @Inject constructor( return FeedListUM( currentDate = getCurrentDate(), feedListSearchBar = FeedListSearchBar( - placeholderText = resourceReference(R.string.markets_search_header_title), + placeholderText = resourceReference( + id = if (feedFeatureToggle.isEarnBlockEnabled) { + R.string.markets_search_title_placeholder + } else { + R.string.markets_search_header_title + }, + ), onBarClick = { analyticsEventHandler.send(FeedAnalyticsEvent.TokenSearchedClicked()) params.feedClickIntents.onMarketOpenClick(null) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt index e155e80515..f362ecfdab 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt @@ -53,6 +53,7 @@ import com.tangem.features.feed.model.market.details.converter.TokenMarketInfoCo import com.tangem.features.feed.model.market.details.formatter.* import com.tangem.features.feed.model.market.details.state.QuotesStateUpdater import com.tangem.features.feed.model.market.details.state.TokenNetworksState +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar import com.tangem.features.feed.ui.market.detailed.state.ExchangesBottomSheetContent import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM import com.tangem.lib.crypto.BlockchainUtils @@ -258,6 +259,10 @@ internal class MarketsTokenDetailsModel @Inject constructor( onScroll = {}, ), onShareClick = ::onShareClick, + feedListSearchBar = FeedListSearchBar( + onBarClick = { params.onMarketOpenClick() }, + placeholderText = resourceReference(id = R.string.markets_search_title_placeholder), + ), ), ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/FeedSearchBar.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/FeedSearchBar.kt new file mode 100644 index 0000000000..34aa87a805 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/FeedSearchBar.kt @@ -0,0 +1,136 @@ +package com.tangem.features.feed.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.ds.topbar.TangemTopBarType +import com.tangem.core.ui.extensions.conditional +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar + +@Composable +internal fun FeedSearchBar( + isSearchBarClickable: Boolean, + feedListSearchBar: FeedListSearchBar, + modifier: Modifier = Modifier, + startContent: @Composable (() -> Unit)? = null, + endContent: @Composable (() -> Unit)? = null, +) { + if (LocalRedesignEnabled.current) { + FeedSearchBarV2( + isSearchBarClickable = isSearchBarClickable, + feedListSearchBar = feedListSearchBar, + modifier = modifier, + startContent = startContent, + endContent = endContent, + ) + } else { + FeedSearchBarV1( + isSearchBarClickable = isSearchBarClickable, + feedListSearchBar = feedListSearchBar, + modifier = modifier, + ) + } +} + +@Composable +private fun FeedSearchBarV1( + isSearchBarClickable: Boolean, + feedListSearchBar: FeedListSearchBar, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .padding(horizontal = 16.dp) + .padding(bottom = 8.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(36.dp)) + .background(color = TangemTheme.colors.field.focused) + .conditional(condition = isSearchBarClickable) { + clickable(onClick = feedListSearchBar.onBarClick) + } + .padding(14.dp), + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size20), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + + SpacerW(14.dp) + + Text( + text = feedListSearchBar.placeholderText.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } +} + +@Composable +private fun FeedSearchBarV2( + isSearchBarClickable: Boolean, + feedListSearchBar: FeedListSearchBar, + modifier: Modifier = Modifier, + startContent: @Composable (() -> Unit)? = null, + endContent: @Composable (() -> Unit)? = null, +) { + TangemTopBar( + modifier = modifier, + startContent = startContent, + endContent = endContent, + type = TangemTopBarType.BottomSheet, + reserveSlotSpace = false, + content = { + Row( + modifier = Modifier + .weight(1f) + .padding( + start = if (startContent != null) TangemTheme.dimens2.x3 else 0.dp, + end = if (endContent != null) TangemTheme.dimens2.x3 else 0.dp, + ) + .clip(CircleShape) + .background(color = TangemTheme.colors2.field.backgroundDefault) + .conditional(condition = isSearchBarClickable) { + clickable(onClick = feedListSearchBar.onBarClick) + } + .padding(TangemTheme.dimens2.x3), + horizontalArrangement = Arrangement.Center, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens2.x5), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_default_24), + tint = TangemTheme.colors2.markers.iconGray, + contentDescription = null, + ) + + SpacerW(TangemTheme.dimens2.x1) + + Text( + text = feedListSearchBar.placeholderText.resolveReference(), + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + }, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt index 5f9756b351..86ca75fa5b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt @@ -22,6 +22,7 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.conditionalCompose import com.tangem.core.ui.extensions.stringReference @@ -29,6 +30,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.* import com.tangem.features.feed.ui.earn.components.* import com.tangem.features.feed.ui.earn.state.* +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar import kotlinx.collections.immutable.persistentListOf private const val EARN_LOAD_MORE_BUFFER = 3 @@ -655,6 +657,10 @@ private fun previewEarnUM( onNetworkFilterClick = {}, onTypeFilterClick = {}, onSliderScroll = {}, + feedListSearchBar = FeedListSearchBar( + placeholderText = TextReference.Str("Search tokens & news"), + onBarClick = {}, + ), ) private const val PLACEHOLDER_ITEMS_COUNT = 8 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt index 974b5a71b2..fd8611ff2b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt @@ -1,6 +1,7 @@ package com.tangem.features.feed.ui.earn.state import androidx.compose.runtime.Immutable +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar @Immutable internal data class EarnUM( @@ -11,4 +12,5 @@ internal data class EarnUM( val onNetworkFilterClick: () -> Unit, val onTypeFilterClick: () -> Unit, val onSliderScroll: () -> Unit, + val feedListSearchBar: FeedListSearchBar, ) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt index e8267f9b85..26f97c3c93 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt @@ -4,34 +4,22 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerW -import com.tangem.core.ui.extensions.conditional -import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.res.LocalMainBottomSheetColor -import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.BaseSearchBarTestTags.SEARCH_BAR import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.ui.components.FeedSearchBar import com.tangem.features.feed.ui.feed.components.* import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState import com.tangem.features.feed.ui.feed.state.FeedListSearchBar @@ -50,8 +38,6 @@ internal fun FeedListHeader( feedListSearchBar = feedListSearchBar, modifier = modifier .drawBehind { drawRect(background) } - .padding(horizontal = 16.dp) - .padding(bottom = 8.dp) .testTag(SEARCH_BAR), ) } @@ -95,39 +81,6 @@ internal fun FeedList( } } -@Composable -private fun FeedSearchBar( - isSearchBarClickable: Boolean, - feedListSearchBar: FeedListSearchBar, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(36.dp)) - .background(color = TangemTheme.colors.field.focused) - .conditional(condition = isSearchBarClickable) { - clickable(onClick = feedListSearchBar.onBarClick) - } - .padding(14.dp), - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size20), - imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - - SpacerW(14.dp) - - Text( - text = feedListSearchBar.placeholderText.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - @Composable private fun FeedListContent( state: FeedListUM, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt index a1183e5467..32a6229d2e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig @@ -23,13 +24,16 @@ import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme @Composable -internal fun Header( +internal fun ColumnScope.Header( onSeeAllClick: () -> Unit, isLoading: Boolean, shouldShowSeeAll: Boolean, title: @Composable () -> Unit, ) { val isRedesignEnabled = LocalRedesignEnabled.current + if (isRedesignEnabled) { + SpacerH(20.dp) + } AnimatedContent(isLoading) { animatedState -> Row( modifier = Modifier diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/DateBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/DateBlock.kt index de6399ee68..b38b335ae1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/DateBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/DateBlock.kt @@ -9,10 +9,20 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme @Composable internal fun DateBlock(currentDate: String) { + if (LocalRedesignEnabled.current) { + DateBlockV2(currentDate) + } else { + DateBlockV1(currentDate) + } +} + +@Composable +private fun DateBlockV1(currentDate: String) { SpacerH(20.dp) Text( modifier = Modifier @@ -30,4 +40,26 @@ internal fun DateBlock(currentDate: String) { style = TangemTheme.typography.h2, color = TangemTheme.colors.text.tertiary, ) +} + +@Composable +private fun DateBlockV2(currentDate: String) { + SpacerH(TangemTheme.dimens2.x1) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + text = stringResourceSafe(R.string.feed_market_and_news), + style = TangemTheme.typography2.headingRegular28, + color = TangemTheme.colors2.text.neutral.primary, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + text = currentDate, + style = TangemTheme.typography2.headingRegular28, + color = TangemTheme.colors2.text.neutral.tertiary, + ) + SpacerH(TangemTheme.dimens2.x6) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt index b24607dc57..832e2e4ea0 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt @@ -71,7 +71,7 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis isLoading = currentChart is MarketChartUM.Loading, ) - SpacerH(12.dp) + SpacerH(if (isRedesignEnabled) 20.dp else 12.dp) Charts( onItemClick = feedListCallbacks.onMarketItemClick, @@ -89,7 +89,7 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis @Suppress("LongMethod") @Composable -internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) { +internal fun ColumnScope.MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) { val isRedesignEnabled = LocalRedesignEnabled.current val onSeeAllClick by rememberUpdatedState { feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt index ed7261d399..65ce49e61e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt @@ -5,8 +5,10 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar import com.tangem.features.feed.ui.market.detailed.state.* import kotlinx.collections.immutable.persistentListOf @@ -50,6 +52,10 @@ internal object MarketsTokenDetailsPreview { onScroll = {}, ), onShareClick = {}, + feedListSearchBar = FeedListSearchBar( + placeholderText = TextReference.Str("Search tokens & news"), + onBarClick = {}, + ), ) val contentState = MarketsTokenDetailsUM( @@ -144,5 +150,9 @@ internal object MarketsTokenDetailsPreview { onScroll = {}, ), onShareClick = {}, + feedListSearchBar = FeedListSearchBar( + placeholderText = TextReference.Str("Search tokens & news"), + onBarClick = {}, + ), ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt index 5f21370028..c6e0bde66a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.markets.PriceChangeInterval +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @@ -29,6 +30,7 @@ internal data class MarketsTokenDetailsUM( val onShouldShowPriceSubtitleChange: (Boolean) -> Unit, val relatedNews: RelatedNews, val onShareClick: () -> Unit, + val feedListSearchBar: FeedListSearchBar, ) { data class ChartState( From f3c2d98214442efb89d20fa2d3a509216fe83d0e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 23 Mar 2026 14:34:39 +0400 Subject: [PATCH 09/75] Updated on 2026-08-14 --- .claude/docs/navigation-graph.md | 471 ++++++++++++++++++ .claude/skills/analyze-logs/SKILL.md | 215 ++++++++ .../main/java/com/tangem/tap/MainActivity.kt | 2 + .../java/com/tangem/tap/TangemApplication.kt | 32 +- .../tangem/tap/features/main/MainViewModel.kt | 1 + .../com/tangem/tap/routing/ProxyAppRouter.kt | 26 +- .../tangem/tap/routing/ProxyAppRouterTest.kt | 245 +++++++++ 7 files changed, 962 insertions(+), 30 deletions(-) create mode 100644 .claude/docs/navigation-graph.md create mode 100644 .claude/skills/analyze-logs/SKILL.md create mode 100644 app/src/test/kotlin/com/tangem/tap/routing/ProxyAppRouterTest.kt diff --git a/.claude/docs/navigation-graph.md b/.claude/docs/navigation-graph.md new file mode 100644 index 0000000000..9e5202e1ca --- /dev/null +++ b/.claude/docs/navigation-graph.md @@ -0,0 +1,471 @@ +# Navigation Graph + +Complete navigation map of the app based on `AppRoute` sealed class and feature-internal routes. + +## 1. All AppRoute Paths + +58 top-level routes defined in `common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt`. + +| # | Route | Path | Description | +|---|-------|------|-------------| +| 1 | `Initial` | `/initial` | App entry point (splash) | +| 2 | `Home` | `/home` | Stories/home screen with launch mode | +| 3 | `Welcome` | `/welcome` | Welcome screen for returning users | +| 4 | `Disclaimer` | `/disclaimer` | Terms of service / disclaimer | +| 5 | `Wallet` | `/wallet` | Main wallet portfolio screen | +| 6 | `CurrencyDetails` | `/currency_details/{walletId}/{currencyId}` | Token/coin detail screen | +| 7 | `Send` | `/send/{walletId}/{currencyId}` | Send cryptocurrency | +| 8 | `Details` | `/details/{walletId}` | Wallet details / settings hub | +| 9 | `DetailsSecurity` | `/details/security` | Security mode settings | +| 10 | `Usedesk` | `/usedesk/{walletId}` | Customer support (Usedesk) | +| 11 | `CardSettings` | `/card_settings/{walletId}` | Card-specific settings | +| 12 | `AppSettings` | `/app_settings` | Global app settings | +| 13 | `ResetToFactory` | `/reset_to_factory/{walletId}/{cardId}/...` | Factory reset flow | +| 14 | `AccessCodeRecovery` | `/access_code_recovery` | Access code recovery | +| 15 | `ManageTokens` | `{source}/manage_tokens/{accountId}` | Add/remove tokens in portfolio | +| 16 | `ChooseManagedTokens` | `/{source}/choose_managed_tokens/...` | Token chooser for send-via-swap | +| 17 | `WalletConnectSessions` | `/wallet_connect_sessions` | WalletConnect sessions list | +| 18 | `QrScanning` | `/{source}/qr_scanning` | QR code scanner | +| 19 | `ReferralProgram` | `/referral_program` | Referral program | +| 20 | `Swap` | `/swap/{fromId}/{toId}/{walletId}/...` | Token swap screen | +| 21 | `AppCurrencySelector` | `/app_currency_selector` | Fiat currency selector | +| 22 | `Staking` | `/staking/{walletId}/{currencyId}/{integrationId}` | Staking screen | +| 23 | `PushNotification` | `/push_notification` | Push notification opt-in | +| 24 | `WalletSettings` | `/wallet_settings/{walletId}` | Per-wallet settings | +| 25 | `WalletBackup` | `/wallet_backup/{walletId}/{coldOption}` | Wallet backup options | +| 26 | `WalletHardwareBackup` | `/wallet_hardware_backup/{walletId}` | Hardware wallet backup | +| 27 | `Markets` | `/markets` | Markets token list | +| 28 | `MarketsTokenDetails` | `/markets_token_details/{tokenId}/{showPortfolio}` | Market token detail | +| 29 | `Onramp` | `/onramp/{walletId}/{symbol}` | Buy crypto (onramp) | +| 30 | `OnrampSuccess` | `/onramp/success/{txId}` | Onramp success screen | +| 31 | `BuyCrypto` | `/buy_crypto/{walletId}` | Buy crypto token selector | +| 32 | `SellCrypto` | `/sell_crypto/{walletId}` | Sell crypto token selector | +| 33 | `SwapCrypto` | `/swap_crypto/{walletId}` | Swap crypto token selector | +| 34 | `Onboarding` | `/onboarding_v2/{mode}` | Onboarding flow (v2) | +| 35 | `Stories` | `/stories$storyId` | Stories / promotional content | +| 36 | `NFT` | `/nft/{walletId}` | NFT collection list | +| 37 | `NFTSend` | `/send/nft/{walletId}/{collection}/{assetId}` | Send NFT | +| 38 | `CreateWalletSelection` | `/create_wallet_selection` | Choose wallet creation type | +| 39 | `CreateWalletStart` | `/create_wallet_start` | Wallet creation intro (cold/hot) | +| 40 | `CreateHardwareWallet` | `/create_hardware_wallet` | Create hardware wallet flow | +| 41 | `CreateMobileWallet` | `/create_mobile_wallet` | Create mobile (hot) wallet | +| 42 | `UpgradeWallet` | `/upgrade_wallet/{walletId}` | Upgrade hot wallet to hardware | +| 43 | `AddExistingWallet` | `/add_existing_wallet` | Import existing wallet | +| 44 | `WalletActivation` | `/wallet_activation/{walletId}` | Activate wallet post-creation | +| 45 | `CreateWalletBackup` | `/create_wallet_backup/{walletId}` | Backup flow for created wallet | +| 46 | `UpdateAccessCode` | `/update_access_code/{walletId}` | Change access code | +| 47 | `ViewPhrase` | `/view_seed_phrase/{walletId}` | View recovery phrase | +| 48 | `ForgetWallet` | `/forget_wallet/{walletId}` | Remove wallet from app | +| 49 | `SendEntryPoint` | `/send_entry_point/{walletId}/{currencyId}` | Send entry with swap option | +| 50 | `CreateAccount` | `/create_account/{walletId}` | Create new account | +| 51 | `EditAccount` | `/edit_account/{accountId}` | Edit account | +| 52 | `AccountDetails` | `/account_details/{accountId}` | Account details screen | +| 53 | `ArchivedAccountList` | `/archived_account/{walletId}` | Archived accounts list | +| 54 | `TangemPayDetails` | `/tangem_pay_details/{walletId}` | Tangem Pay card details | +| 55 | `TangemPayOnboarding` | `/tangem_pay_onboarding/{mode}` | Tangem Pay onboarding | +| 56 | `Kyc` | `/kyc` | KYC verification | +| 57 | `YieldSupplyEntry` | `/yield_supply_entry/{walletId}/{symbol}` | Yield/supply entry point | +| 58 | `NewsDetails` | `/news_details/{newsId}` | News article detail | + +## 2. Navigation Edges + +Each entry shows: **Source route** → target routes it can navigate to (via `push` or `replaceAll`). + +### Initial / Bootstrap + +| Source | Target | Method | Trigger | +|--------|--------|--------|---------| +| `Initial` | `Home`, `Welcome`, `Disclaimer`, `Onboarding`, etc. | `replaceAll` | App startup (DefaultRoutingComponent) | + +### Home + +| Target | Method | Trigger | +|--------|--------|---------| +| `ManageTokens(STORIES)` | push | After scan, manage tokens | +| `CreateWalletStart` | push | Create wallet from home | +| `Wallet` | replaceAll | After wallet saved / already saved | + +### Welcome + +| Target | Method | Trigger | +|--------|--------|---------| +| `CreateWalletSelection` | push | "Add new wallet" button | +| `Home()` | replaceAll | When wallets list becomes empty | +| `Wallet` | replaceAll | After scan / wallet unlock / biometric | + +### Disclaimer + +| Target | Method | Trigger | +|--------|--------|---------| +| `PushNotification(Stories)` | push | After accepting TOS (stories flow) | +| `Home()` | replaceAll | After accepting TOS (non-stories flow) | + +### Wallet (main portfolio) + +| Target | Method | Trigger | +|--------|--------|---------| +| `Details` | push | Open wallet details | +| `ManageTokens(ACCOUNT)` | push | Manage tokens for account | +| `Onboarding` | push | Continue backup / onboarding | +| `CurrencyDetails` | push | Tap on a token | +| `Home` | push | Open stories | +| `NFT` | push | Open NFT collection | +| `TangemPayOnboarding` | push | Tangem Pay banner | +| `TangemPayDetails` | push | Tangem Pay card details | +| `YieldSupplyEntry` | push | Yield supply action | +| `QrScanning(MainScreen)` | push | QR scanner | +| `Send` | push | Send from QR / action | +| `WalletBackup` | push | Backup warning banner | + +### CurrencyDetails (token details) + +| Target | Method | Trigger | +|--------|--------|---------| +| `Onramp` | push | Buy action | +| `SendEntryPoint` | push | Send action | +| `Swap` | push | Swap action | +| `CurrencyDetails` | push | Navigate to related token (from staking router) | +| `Staking` | push | Open staking (from token details router) | + +### Details (wallet details hub) + +| Target | Method | Trigger | +|--------|--------|---------| +| `CreateWalletSelection` | push | Add new wallet | +| `WalletSettings` | push | Open wallet settings | +| `WalletConnectSessions` | push | WalletConnect item | +| `AppSettings` | push | App settings item | +| `Disclaimer(isTosAccepted=true)` | push | View TOS | +| `Usedesk` | push | Customer support | +| `TangemPayOnboarding(FromBannerInSettings)` | push | Tangem Pay banner | + +### WalletSettings + +| Target | Method | Trigger | +|--------|--------|---------| +| `ReferralProgram` | push | Referral program | +| `WalletHardwareBackup` | push | Hardware backup | +| `CardSettings` | push | Card settings | +| `ForgetWallet` | push | Delete/forget wallet | +| `ViewPhrase` | push | View seed phrase | +| `AccountDetails` | push | Open account details | +| `ArchivedAccountList` | push | View archived accounts | +| `CreateAccount` | push | Create new account | +| `Home()` | replaceAll | After wallet deletion completes | + +### WalletBackup + +| Target | Method | Trigger | +|--------|--------|---------| +| `WalletActivation` | push | Start activation (no backup) | +| `ViewPhrase` | push | View phrase option | +| `WalletHardwareBackup` | push | Hardware backup option | + +### WalletHardwareBackup + +| Target | Method | Trigger | +|--------|--------|---------| +| `CreateHardwareWallet` | push | Create new hardware wallet | +| `UpgradeWallet` | push | Upgrade current hot wallet | +| `CreateWalletBackup` | push | Backup existing wallet | + +### CreateWalletSelection + +| Target | Method | Trigger | +|--------|--------|---------| +| `CreateMobileWallet` | push | Choose mobile wallet | +| `CreateHardwareWallet` | push | Choose hardware wallet | + +### CreateWalletStart + +| Target | Method | Trigger | +|--------|--------|---------| +| `CreateMobileWallet` | push | Create mobile wallet | +| `Wallet` | replaceAll | After wallet creation completes | + +### CreateMobileWallet + +| Target | Method | Trigger | +|--------|--------|---------| +| `AddExistingWallet` | push | Import existing wallet | +| `Wallet` | replaceAll | After creation completes | + +### CreateHardwareWallet + +| Target | Method | Trigger | +|--------|--------|---------| +| `Wallet` | replaceAll | After hardware wallet created | + +### AddExistingWallet + +| Target | Method | Trigger | +|--------|--------|---------| +| `Wallet` | replaceAll | After import completes | + +### UpgradeWallet + +| Target | Method | Trigger | +|--------|--------|---------| +| `Onboarding(UpgradeHotWallet)` | push | Start upgrade onboarding | + +### CreateWalletBackup + +| Target | Method | Trigger | +|--------|--------|---------| +| `UpgradeWallet` | push | After backup, continue to upgrade | + +### AccountDetails + +| Target | Method | Trigger | +|--------|--------|---------| +| `EditAccount` | push | Edit account | + +### ForgetWallet + +| Target | Method | Trigger | +|--------|--------|---------| +| `Home()` | replaceAll | After wallet forgotten | + +### WalletConnectSessions + +| Target | Method | Trigger | +|--------|--------|---------| +| `QrScanning(WalletConnect)` | push | Scan WC QR code | + +### Onboarding + +| Target | Method | Trigger | +|--------|--------|---------| +| `Home()` | replaceAll | Onboarding completed (no wallets) | +| `Wallet` | replaceAll | Onboarding completed (has wallets) | + +### PushNotification + +| Target | Method | Trigger | +|--------|--------|---------| +| `Home()` | replaceAll | After push notification opt-in (via nextRoute param) | + +### Staking + +| Target | Method | Trigger | +|--------|--------|---------| +| `CurrencyDetails` | push | Back to token details | + +### TangemPayDetails + +| Target | Method | Trigger | +|--------|--------|---------| +| `Swap` | push | Top up / withdraw via swap | + +### NFT + +| Target | Method | Trigger | +|--------|--------|---------| +| `NFTSend` | push | Send NFT | + +### Send (notifications) + +| Target | Method | Trigger | +|--------|--------|---------| +| `CurrencyDetails` | push | Navigate to fee token | + +### SwapCrypto / BuyCrypto / SellCrypto + +| Target | Method | Trigger | +|--------|--------|---------| +| `Swap` | push | After token selection (SwapCrypto) | +| `Onramp` | push | After token selection (BuyCrypto/SellCrypto) | + +### Deep Link Handlers (push to AppRoute) + +| Handler | Target Route | +|---------|-------------| +| `OnrampDeepLinkHandler` | Processes onramp callback params | +| `SellRedirectDeepLinkHandler` | `Send` (with sell redirect params) | +| `BuyDeepLinkHandler` | `BuyCrypto` | +| `SellDeepLinkHandler` | `SellCrypto` | +| `SwapDeepLinkHandler` | `SwapCrypto` | +| `ReferralDeepLinkHandler` | Referral handling | +| `WalletDeepLinkHandler` | Wallet handling | +| `TokenDetailsDeepLinkHandler` | `CurrencyDetails` | +| `StakingDeepLinkHandler` | `Staking` | +| `MarketsDeepLinkHandler` | `Markets` | +| `MarketsTokenDetailDeepLinkHandler` | `MarketsTokenDetails` | +| `WalletConnectDeepLinkHandler` | WalletConnect pairing | +| `PromoDeeplinkHandler` | Promo handling | +| `OnboardVisaDeepLinkHandler` | `TangemPayOnboarding` | +| `NewsDetailsDeepLinkHandler` | `NewsDetails` | + +## 3. Nested Routes (Feature-Internal Navigation) + +### OnboardingRoute +**File:** `features/onboarding-v2/impl/.../routing/OnboardingRoute.kt` + +| Route | Description | +|-------|-------------| +| `None` | Initial empty state | +| `Note` | Single-card onboarding note | +| `MultiWallet` | Multi-wallet onboarding (with seed phrase flow option) | +| `Visa` | Visa card onboarding | +| `Twins` | Twin cards onboarding | +| `ManageTokens` | Token management during onboarding | +| `AskBiometry` | Biometry setup prompt | +| `Done` | Onboarding completion | + +### WalletRoute +**File:** `features/wallet/impl/.../navigation/WalletRoute.kt` + +| Route | Description | +|-------|-------------| +| `Wallet` | Main wallet view | +| `OrganizeTokens` | Reorder tokens in portfolio | + +### SendEntryRoute +**File:** `features/send-v2/api/.../entry/SendEntryRoute.kt` + +| Route | Description | +|-------|-------------| +| `Send` | Direct send flow | +| `SendWithSwap` | Send with swap option | +| `ChooseToken` | Token chooser for send-via-swap | + +### CommonSendRoute (Send internal) +Used internally by `SendModel` and `NFTSendModel`: +- `Amount` → `Destination` → `Confirm` → `ConfirmSuccess` +- Edit mode: `Confirm` → `Destination(edit)` or `Amount(edit)` + +### FeeSelectorRoute (Send internal) +- `ChooseToken` — select fee token +- `ChooseSpeed` — select fee speed + +### WcInnerRoute (WalletConnect) +**File:** `features/walletconnect/impl/.../routing/WcInnerRoute.kt` + +| Route | Description | +|-------|-------------| +| `Method.Send` | WC send transaction | +| `Method.SignMessage` | WC sign message | +| `Method.AddNetwork` | WC add network | +| `Method.SwitchNetwork` | WC switch network | +| `Pair` | WC pairing request | +| `UnsupportedMethodAlert` | Unsupported method alert | +| `WcDappDisconnected` | DApp disconnected alert | +| `TangemUnsupportedNetwork` | Unsupported network alert | +| `RequiredAddNetwork` | Required network add | +| `RequiredReconnectWithNetwork` | Required network reconnect | + +### TangemPayDetailsInnerRoute +**File:** `features/tangempay/details/impl/.../navigation/TangemPayDetailsInnerRoute.kt` + +| Route | Description | +|-------|-------------| +| `Details` | Main details view | +| `ChangePIN` | Change PIN flow | +| `ChangePINSuccess` | PIN change success | +| `AddToWallet` | Add card to device wallet | + +Transitions: `Details` → `ChangePIN` → `ChangePINSuccess`, `Details` → `AddToWallet` + +### FeedEntryRoute +**File:** `features/feed/api/.../components/FeedEntryRoute.kt` + +| Route | Description | +|-------|-------------| +| `MarketTokenDetails` | Market token detail view | +| `MarketTokenList` | Markets list | +| `NewsDetail` | News article detail | + +### CreateWalletBackupRoute +**File:** `features/hot-wallet/impl/.../createwalletbackup/routing/CreateWalletBackupRoute.kt` + +| Route | Description | +|-------|-------------| +| `RecoveryPhraseStart` | Backup intro | +| `RecoveryPhrase` | Show recovery phrase | +| `ConfirmBackup` | Confirm backup | +| `BackupCompleted` | Backup complete (with upgrade/last screen flags) | + +Transitions: `RecoveryPhraseStart` → `RecoveryPhrase` → `ConfirmBackup` → `BackupCompleted` + +### AddExistingWalletRoute +**File:** `features/hot-wallet/impl/.../addexistingwallet/entry/routing/AddExistingWalletRoute.kt` + +| Route | Description | +|-------|-------------| +| `Import` | Seed phrase import | +| `BackupCompleted` | Backup completed | +| `SetAccessCode` | Set access code | +| `ConfirmAccessCode` | Confirm access code | +| `PushNotifications` | Push notification opt-in | +| `SetupFinished` | Setup complete | + +Transitions: `Import` → `BackupCompleted` → `SetAccessCode` → `ConfirmAccessCode` → `PushNotifications` → `SetupFinished` + +### UpdateAccessCodeRoute +**File:** `features/hot-wallet/impl/.../updateaccesscode/routing/UpdateAccessCodeRoute.kt` + +| Route | Description | +|-------|-------------| +| `SetAccessCode` | Enter new access code | +| `ConfirmAccessCode` | Confirm new access code | +| `SetupFinished` | Update complete | + +Transitions: `SetAccessCode` → `ConfirmAccessCode` → `SetupFinished` + +### WalletActivationRoute +**File:** `features/hot-wallet/impl/.../walletactivation/entry/routing/WalletActivationRoute.kt` + +| Route | Description | +|-------|-------------| +| `ManualBackupStart` | Backup intro | +| `ManualBackupPhrase` | Show recovery phrase | +| `ManualBackupCheck` | Verify backup | +| `ManualBackupCompleted` | Backup success | +| `SetAccessCode` | Set access code | +| `ConfirmAccessCode` | Confirm access code | +| `PushNotifications` | Push notification opt-in | +| `SetupFinished` | Activation complete | + +Transitions: `ManualBackupStart` → `ManualBackupPhrase` → `ManualBackupCheck` → `ManualBackupCompleted` → `SetAccessCode` → `ConfirmAccessCode` → `PushNotifications` → `SetupFinished` + +## 4. Deep Links + +### URI Schemes + +| Scheme | Value | Usage | +|--------|-------|-------| +| `Tangem` | `tangem://` | Primary app deep links | +| `WalletConnect` | `wc://` | WalletConnect pairing | +| `Https` | `https://` | Web links (tangem.com) | + +### Tangem Scheme Routes (`tangem://{host}`) + +| Host | Handler | Target | +|------|---------|--------| +| `onramp` | `OnrampDeepLinkHandler` | Onramp callback processing | +| `redirect_sell` | `SellRedirectDeepLinkHandler` | `Send` (sell redirect with tx params) | +| `redirect` | — | Buy redirect (no-op) | +| `buy` | `BuyDeepLinkHandler` | `BuyCrypto` | +| `sell` | `SellDeepLinkHandler` | `SellCrypto` | +| `swap` | `SwapDeepLinkHandler` | `SwapCrypto` | +| `referral` | `ReferralDeepLinkHandler` | Referral flow | +| `main` | `WalletDeepLinkHandler` | Wallet screen | +| `token` | `TokenDetailsDeepLinkHandler` | `CurrencyDetails` | +| `staking` | `StakingDeepLinkHandler` | `Staking` | +| `markets` | `MarketsDeepLinkHandler` | `Markets` | +| `token_chart` | `MarketsTokenDetailDeepLinkHandler` | `MarketsTokenDetails` | +| `wc` | `WalletConnectDeepLinkHandler` | WalletConnect pairing | +| `promo` | `PromoDeeplinkHandler` | Promo handling | +| `onboard-visa` | `OnboardVisaDeepLinkHandler` | `TangemPayOnboarding` | + +### HTTPS Routes (`https://tangem.com/...`) + +| Path prefix | Handler | Target | +|-------------|---------|--------| +| `/pay-app` | `OnboardVisaDeepLinkHandler` | `TangemPayOnboarding` | +| `/news` | `NewsDetailsDeepLinkHandler` | `NewsDetails` | + +### Deep Link Readiness + +Deep links are only processed when the app is on a "ready" route. These routes **block** deep link processing: +- `Initial`, `Home`, `Welcome`, `PushNotification`, `Disclaimer`, `Stories`, `Onboarding` \ No newline at end of file diff --git a/.claude/skills/analyze-logs/SKILL.md b/.claude/skills/analyze-logs/SKILL.md new file mode 100644 index 0000000000..4ecc6e0104 --- /dev/null +++ b/.claude/skills/analyze-logs/SKILL.md @@ -0,0 +1,215 @@ +--- +name: analyze-logs +description: Analyze Tangem app user logs — extract device info, navigation path, errors, and key events timeline. Use when user provides a log file for bug investigation. +allowed-tools: Read, Grep +argument-hint: /path/to/logfile.txt [/path/to/logs.rtf] +--- + +Analyze the Tangem app user log file at path: `$ARGUMENTS` + +## File Input + +The user provides one or two file paths: +- **Log file** (`.txt`) — main application log, always required +- **User info file** (`.rtf` or `.txt`) — optional, contains card/device/error info from the user's feedback email + +If two paths are provided, the first is the log file and the second is the user info file. + +**If only the log file is provided**, ask the user if they have a user info file (`logs.rtf` or `logs.txt`). If they don't have it or don't respond, fill Device Context, Card Info, and Transaction Context sections from the log file data (Steps 2+3). Mark fields that could not be determined as "N/A". + +## User Info File (logs.rtf / logs.txt) + +If a user info file is provided, Read it and extract the plain text fields. The file contains structured key-value pairs like: + +``` +Card ID: AF36000002151580 +Firmware version: 6.33r +Linked cards count: 2 +Has seed phrase: true +Signed hashes [secp256k1]: 0 +---------- +Blockchain: Polygon +Explorer link: https://polygonscan.com/address/0x... +Derivation path: m/44'/60'/0'/0/0 +Host: https://rpc-mainnet.matic.quiknode.pro/ +Token: USDC +Error: Could not construct a recoverable key. +---------- +Source address: 0x... +Destination address: 0x... +Amount: 11.319684 +Fee: 0.004973 +---------- +Phone model: SM-S921B +OS version: 36 +App version: 5.34.1 +``` + +Extract all fields and include them in the **Device Context**, **Card Info**, and **Analysis Summary** sections of the report. If the RTF contains an `Error:` field, treat it as a key clue for the investigation. + +Note: RTF files contain formatting markup (`\cb3`, `\cf4`, `{\field{...}}`). Ignore all RTF tags — only extract the plain text values after each colon. + +## Log Format + +Each line follows the pattern: +``` +DD.MM HH:MM:SS.mmm: TAG Message +``` + +- Date format: `DD.MM` (day.month), no year — infer from context +- Multi-line entries (JSON bodies, stack traces) continue without the timestamp prefix +- Sensitive data is masked with `******` +- Continuation lines may start with `|` for structured data: `|- Duration millis: 300000` + +## Analysis Steps + +Use `head_limit` on every Grep call to protect context from overload. + +### Step 1: Setup + +1. **Log time range:** Read the first and last lines with dates (format `DD.MM`) +2. **Ask the user** (report time range, then ask): + - Date range to focus on (or "all" for the full file) + - Focus area: `Wallet`, `WalletConnect`, `Express (Onramp/Buy, Offramp/Sell, Swap/Exchange)`, `TangemPay`, `Feed`, `Markets`, `Settings`, `Referral`, `Staking`, `Onboarding`, `Send/Transactions`, `NFT`, or `all` + - Remember the chosen area as **FOCUS_AREA** +3. **Determine line range** (skip if "all"): + - Parse input into `DD.MM` patterns (`10-13.03` → start `10.03`, end `13.03`; `last day` → last date; `last 3 days` → 3 days before last) + - Find **START_LINE**: Grep `^START_DATE` (head_limit: 1, -n: true) + - Find **END_LINE**: Grep `^NEXT_DATE` (head_limit: 1, -n: true). If not found, END_LINE = end of file + - Report: "Focusing on lines START_LINE–END_LINE covering DD.MM–DD.MM" + +### Steps 2+3+4+5+6: Main Analysis (all in parallel) + +Launch ALL Grep calls below in parallel. Steps 2+3 search the **full file** (device info may be before the date range). Steps 4+5+6 use `offset: START_LINE` to stay within the date range. + +**Device Context (full file):** +- `PATCH.*user-wallets/applications` (head_limit: 5, -A 10) — Read JSON body to extract `systemVersion`, `version`, `language`, `timezone` +- `ip_address` (head_limit: 5, -A 20) — extract `alpha2`, `country`, `isBuyAllowed`, `isSellAllowed` + +**Card Info (full file):** +- `CardSDK_Tlv.*TAG_Firmware` (head_limit: 20) +- `CardSDK_Tlv.*TAG_SettingsMask` (head_limit: 20) +- `CardSDK_Tlv.*TAG_IsActivated` (head_limit: 20) +- `CardSDK_Tlv.*TAG_ManufacturerName` (head_limit: 20) + +**Navigation (offset: START_LINE):** +- `AppRouter` (head_limit: 200) — if FOCUS_AREA is `all` or navigation-heavy (Wallet, Onboarding, Send/Transactions), also Read `.claude/docs/navigation-graph.md` to cross-reference routes + +**Errors (offset: START_LINE, head_limit: 50 each, -n: true):** +- HTTP errors: `<-- [45]\d{2}` +- Domain errors: `DomainError` +- App exceptions: `\bException\b` +- Biometric errors: `onAuthenticationError` +- Tangem Pay errors: `Failed checkCustomerWallet` + +**Session timeline (offset: START_LINE, head_limit: 50 each):** +- `MainActivity.*onCreate` — app session start +- `MainActivity.*Splash screen` — splash screen installed/dismissed +- `MainActivity.*onNewIntent` — deep link or push notification +- `CardSDK_Session.*start card session` — NFC session starts + +**Error filtering:** When processing error results, skip these noisy matches: +- `java.io.IOException: Canceled` — normal request cancellation +- `HttpException(code=304` — HTTP "Not Modified" +- Bare stacktrace lines starting with `\tat` +- `<-- HTTP FAILED: java.io.IOException: Canceled` + +### Step 7: Deep Dive + +For each significant error found above: +1. Note the error's line number from Grep output (`-n: true`) +2. Use Read with `offset: ERROR_LINE - 100, limit: 200` to get ~200 lines of context +3. In that context, look for navigation events, API calls, and redux actions + +## Key Tags Reference + +| Tag | Purpose | +|-----|---------| +| `MainActivity` | Activity lifecycle, splash screen, onNewIntent | +| `AppRouter` | Navigation: Push, Pop, Replace | +| `NetworkLogs` | HTTP requests/responses (OkHttp) | +| `BlockchainSDK_NETWORK` | Blockchain RPC calls | +| `CardSDK_Tlv` | NFC card data (firmware, settings) | +| `CardSDK_Session` | NFC session lifecycle | +| `CardSDK_Biometric` | Biometric authentication | + +## Common Error Patterns + +| Pattern | Meaning | +|---------|---------| +| `HttpException(code=4xx/5xx, errorBody={...})` | API error with structured body | +| `DomainError(description=...)` | App-level domain error | +| `<-- HTTP FAILED: java.io.IOException: Canceled` | Cancelled network request (noise) | +| `<-- 429` | Rate limiting | +| `onAuthenticationError` | Biometric auth failure | + +## Output Template + +Structure your report EXACTLY as follows: + +``` +# Log Analysis Report + +## Device Context +| Parameter | Value | +|-----------|-------| +| App version | ... | +| Android version | ... | +| Phone model | ... (from logs.rtf if available) | +| Language | ... | +| Timezone | ... | +| Country | ... | +| Log time range | DD.MM HH:MM — DD.MM HH:MM | + +## Card Info +| Parameter | Value | +|-----------|-------| +| Card ID | ... (from logs.rtf if available) | +| Firmware | ... | +| Manufacturer | ... | +| Is activated | ... | +| Linked cards | ... (from logs.rtf if available) | +| Has seed phrase | ... (from logs.rtf if available) | + +## Transaction Context (from logs.rtf, if available) +| Parameter | Value | +|-----------|-------| +| Blockchain | ... | +| Token | ... | +| Source address | ... | +| Destination address | ... | +| Amount | ... | +| Fee | ... | +| Error | ... | + +## Navigation Path +1. [HH:MM:SS] Screen (Push/Pop/Replace) +2. ... + +**Summary:** Brief description of the user's journey. + +## Errors Found + +### HTTP Errors +| Time | URL | Status | Details | +|------|-----|--------|---------| + +### Domain Errors +| Time | Component | Error | +|------|-----------|-------| + +### Other Errors +| Time | Type | Details | +|------|------|---------| + +## Key Events Timeline +| Time | Event | Details | +|------|-------|---------| +(chronological: app starts, card sessions, navigation, errors, notable API calls) + +## Analysis Summary +(2-3 paragraphs: what the user was doing, what broke, probable cause, recommendations. +If FOCUS_AREA was specified, emphasize errors, navigation, and API calls related to that area.) +``` + +If a section has no data, write "None found" instead of omitting it. \ 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 dd856fb233..f6a2ff3217 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -174,6 +174,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { installAppTheme() val splashScreen = installSplashScreen() + TangemLogger.i("Splash screen installed") enableEdgeToEdge( navigationBarStyle = SystemBarStyle.auto( @@ -350,6 +351,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) + TangemLogger.i("onNewIntent: data=${intent.data}, extras=${intent.extras?.keySet()}") val isFromPush = intent.extras?.containsKey(OPENED_FROM_GCM_PUSH) == true if (isFromPush) { diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 835298b890..01efaeb7bf 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -270,22 +270,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. } } - private fun updateLogFiles() { - appLogsStore.deleteOldLogsFile() - - if (!BuildConfig.TESTER_MENU_ENABLED) { - appLogsStore.deleteLastLogFile() - } - - // Temporally logs are not saved - // scope.launch { - // if (!appPreferencesStore.getSyncOrDefault(WAS_LOG_FILE_CLEARED, false)) { - // appLogsStore.deleteLastLogFile() - // appPreferencesStore.store(WAS_LOG_FILE_CLEARED, true) - // } - // } - } - /** * Initialize components that need to be initialized before [super.onCreate] is called */ @@ -389,6 +373,22 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. ) } + private fun updateLogFiles() { + appLogsStore.deleteOldLogsFile() + + if (!BuildConfig.TESTER_MENU_ENABLED) { + appLogsStore.deleteLastLogFile() + } + + // Temporarily logs are not saved + // scope.launch { + // if (!appPreferencesStore.getSyncOrDefault(WAS_LOG_FILE_CLEARED, false)) { + // appLogsStore.deleteLastLogFile() + // appPreferencesStore.store(WAS_LOG_FILE_CLEARED, true) + // } + // } + } + override fun newImageLoader(): ImageLoader { return createCoilImageLoader( context = this, 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 be57cd0aa2..7b156a7889 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 @@ -149,6 +149,7 @@ internal class MainViewModel @Inject constructor( // await while initial route stack is initialized appRouterConfig.initializedState.first { it } + TangemLogger.withTag("MainActivity").i("Splash screen dismissed") isSplashScreenShown = false } } diff --git a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt index d9cd7759f2..a78fc262d8 100644 --- a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt +++ b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt @@ -7,6 +7,7 @@ import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.core.decompose.navigation.Router import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.logging.TangemLogger import com.tangem.wallet.R import kotlinx.coroutines.CoroutineScope @@ -19,6 +20,8 @@ internal class ProxyAppRouter( private val analyticsExceptionHandler: AnalyticsExceptionHandler, ) : AppRouter { + private val logger = TangemLogger.withTag("AppRouter") + private val routerScope: CoroutineScope get() = requireNotNull(config.routerScope) { "Router scope is not set in config" @@ -47,12 +50,8 @@ internal class ProxyAppRouter( } override fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) { - safeNavigate(onComplete, message = "Replace all routes with $routes") { - runCatching { - innerRouter.replaceAll(*routes, onComplete = onComplete) - }.getOrElse { - TangemLogger.e("Error", it) - } + safeNavigate(onComplete, message = "Replace all routes with ${routes.toList().ifEmpty { "" }}") { + innerRouter.replaceAll(*routes, onComplete = onComplete) } } @@ -76,21 +75,20 @@ internal class ProxyAppRouter( private fun safeNavigate(onComplete: (isSuccess: Boolean) -> Unit, message: String, block: () -> Unit) { routerScope.launch(dispatchers.mainImmediate) { - TangemLogger.i(message) + logger.i(message) - try { - block() - } catch (e: Throwable) { - TangemLogger.e("Error", e) - onComplete(false) - } + runSuspendCatching(block = { block() }) + .onFailure { throwable -> + logger.e(messageString = "Error", throwable = throwable) + onComplete(false) + } } } override fun defaultCompletionHandler(isSuccess: Boolean, errorMessage: String) { if (!isSuccess) { analyticsExceptionHandler.sendException(ExceptionAnalyticsEvent(RuntimeException(errorMessage))) - TangemLogger.w(errorMessage) + logger.w(errorMessage) with(receiver = config.snackbarHandler ?: return) { showSnackbar( diff --git a/app/src/test/kotlin/com/tangem/tap/routing/ProxyAppRouterTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/ProxyAppRouterTest.kt new file mode 100644 index 0000000000..264a9b3f0a --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/routing/ProxyAppRouterTest.kt @@ -0,0 +1,245 @@ +package com.tangem.tap.routing + +import com.google.common.truth.Truth.assertThat +import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent +import com.tangem.core.decompose.navigation.Router +import com.tangem.tap.common.SnackbarHandler +import com.tangem.tap.routing.configurator.AppRouterConfig +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@OptIn(ExperimentalCoroutinesApi::class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class ProxyAppRouterTest { + + private val innerRouter = mockk(relaxed = true) + private val snackbarHandler = mockk(relaxed = true) + private val analyticsExceptionHandler = mockk(relaxed = true) + private val dispatchers = TestingCoroutineDispatcherProvider() + + private val config = mockk(relaxed = true) { + every { componentRouter } returns innerRouter + every { stack } returns listOf(AppRoute.Wallet) + every { snackbarHandler } returns this@ProxyAppRouterTest.snackbarHandler + every { initializedState } returns MutableStateFlow(true) + } + + @AfterEach + fun tearDown() { + clearMocks(innerRouter, snackbarHandler, analyticsExceptionHandler, config) + every { config.componentRouter } returns innerRouter + every { config.stack } returns listOf(AppRoute.Wallet) + every { config.snackbarHandler } returns snackbarHandler + every { config.initializedState } returns MutableStateFlow(true) + } + + private fun createRouter(routerScope: CoroutineScope): ProxyAppRouter { + every { config.routerScope } returns routerScope + return ProxyAppRouter(config, dispatchers, analyticsExceptionHandler) + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Push { + + @Test + fun `delegates to inner router`() = runTest { + // Arrange + val router = createRouter(this) + val route = AppRoute.AppSettings + + // Act + router.push(route) + + // Assert + verify { innerRouter.push(route, any()) } + } + + @Test + fun `calls onComplete false when inner router throws`() = runTest { + // Arrange + val router = createRouter(this) + every { innerRouter.push(any(), any()) } throws RuntimeException("Navigation error") + var result: Boolean? = null + + // Act + router.push(AppRoute.AppSettings) { result = it } + + // Assert + assertThat(result).isFalse() + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ReplaceCurrent { + + @Test + fun `delegates to inner router`() = runTest { + // Arrange + val router = createRouter(this) + val route = AppRoute.AppSettings + + // Act + router.replaceCurrent(route) + + // Assert + verify { innerRouter.replaceCurrent(route, any()) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ReplaceAll { + + @Test + fun `delegates to inner router`() = runTest { + // Arrange + val router = createRouter(this) + val route = AppRoute.Wallet + + // Act + router.replaceAll(route) + + // Assert + verify { innerRouter.replaceAll(route, onComplete = any()) } + } + + @Test + fun `catches exception silently via safeNavigate`() = runTest { + // Arrange + val router = createRouter(this) + every { innerRouter.replaceAll(*anyVararg(), onComplete = any()) } throws RuntimeException("Error") + + // Act + val actual = runCatching { router.replaceAll(AppRoute.Wallet) }.isSuccess + + // Assert + assertThat(actual).isTrue() + verify { innerRouter.replaceAll(AppRoute.Wallet, onComplete = any()) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Pop { + + @Test + fun `delegates to inner router`() = runTest { + // Arrange + val router = createRouter(this) + + // Act + router.pop() + + // Assert + verify { innerRouter.pop(any()) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class PopTo { + + @Test + fun `delegates to inner router with route`() = runTest { + // Arrange + val router = createRouter(this) + val route = AppRoute.Wallet + + // Act + router.popTo(route) + + // Assert + verify { innerRouter.popTo(route, any()) } + } + + @Test + fun `delegates to inner router with routeClass`() = runTest { + // Arrange + val router = createRouter(this) + + // Act + router.popTo(AppRoute.Wallet::class) + + // Assert + verify { innerRouter.popTo(AppRoute.Wallet::class, any()) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class DefaultCompletionHandler { + + @Test + fun `does nothing on success`() { + // Arrange + val router = createRouter(mockk()) + + // Act + router.defaultCompletionHandler(isSuccess = true, errorMessage = "error") + + // Assert + verify(exactly = 0) { analyticsExceptionHandler.sendException(any()) } + verify(exactly = 0) { snackbarHandler.showSnackbar(text = any(), buttonTitle = any(), action = any()) } + } + + @Test + fun `sends analytics and shows snackbar on failure`() { + // Arrange + val router = createRouter(mockk()) + + // Act + router.defaultCompletionHandler(isSuccess = false, errorMessage = "Navigation failed") + + // Assert + verify { analyticsExceptionHandler.sendException(any()) } + verify { snackbarHandler.showSnackbar(text = any(), buttonTitle = any(), action = any()) } + } + + @Test + fun `sends analytics without snackbar when handler is null`() { + // Arrange + every { config.snackbarHandler } returns null + val router = createRouter(mockk()) + + // Act + router.defaultCompletionHandler(isSuccess = false, errorMessage = "Navigation failed") + + // Assert + verify { analyticsExceptionHandler.sendException(any()) } + verify(exactly = 0) { snackbarHandler.showSnackbar(text = any(), buttonTitle = any(), action = any()) } + } + } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class Stack { + + @Test + fun `returns config stack`() { + // Arrange + val router = createRouter(mockk()) + val expected = listOf(AppRoute.Wallet) + + // Act + val actual = router.stack + + // Assert + assertThat(actual).isEqualTo(expected) + } + } +} \ No newline at end of file From f192c85914f1ad31e36271cfacbe8ea1ee71dbf6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Mar 2026 12:05:48 +0500 Subject: [PATCH 10/75] Updated on 2026-08-14 --- .../account/AccountIconItemStateConverter.kt | 31 +- .../CreatePaymentAccountNotification.kt | 98 +++++ .../com/tangem/datasource/di/MoshiModule.kt | 15 +- ...usDM.kt => PaymentAccountStatusValueDM.kt} | 45 ++- .../drawable/img_tangem_pay_visa_banner.webp | Bin 0 -> 3402 bytes data/account/build.gradle.kts | 4 + .../DefaultSingleAccountListProducer.kt | 39 +- .../DefaultSingleAccountListProducerTest.kt | 8 + .../PaymentAccountStatusDMConverter.kt | 67 ---- .../PaymentAccountStatusValueDMConverter.kt | 139 +++++++ .../tangem/data/pay/di/TangemPayDataModule.kt | 6 +- .../DefaultPaymentAccountStatusFetcher.kt | 179 +++++---- .../DefaultPaymentAccountStatusProducer.kt | 16 +- .../repository/DefaultOnboardingRepository.kt | 18 +- .../pay/store/PaymentAccountStatusesStore.kt | 45 ++- .../domain/account/models/AccountList.kt | 16 +- domain/account/status/build.gradle.kts | 3 + .../DefaultSingleAccountStatusListProducer.kt | 125 ++++-- .../tangem/domain/models/account/Account.kt | 11 +- .../domain/models/account/AccountStatus.kt | 3 +- .../account/PaymentAccountStatusValue.kt | 193 ++++++++++ .../tangem/domain/pay/PaymentAccountStatus.kt | 66 ---- .../pay/flow/PaymentAccountStatusProducer.kt | 4 +- .../pay/flow/PaymentAccountStatusSupplier.kt | 12 +- .../tangem/domain/pay/model/CustomerInfo.kt | 5 + .../TangemPayMainScreenCustomerInfoUseCase.kt | 24 +- .../selector/PortfolioSelectorModel.kt | 2 +- .../tokenlist/model/OnrampTokenListModel.kt | 4 +- .../feature/swap/domain/SwapInteractorImpl.kt | 3 +- features/tangempay/main/api/build.gradle.kts | 2 + .../component/TangemPayMainBlockComponent.kt | 19 + .../tangempay/entity/TangemPayMainUM.kt | 26 ++ features/tangempay/main/impl/build.gradle.kts | 3 +- .../DefaultTangemPayMainBlockComponent.kt | 37 ++ .../tangempay/di/TangemPayMainModule.kt | 17 + .../tangempay/ui/TangemPayMainBlockContent.kt | 355 ++++++++++++++++++ features/wallet/impl/build.gradle.kts | 1 + .../wallet/child/wallet/WalletComponent.kt | 14 +- .../wallet/child/wallet/model/WalletModel.kt | 29 +- .../model/intents/TangemPayClickIntents.kt | 26 +- .../preview/WalletScreenPreviewDataLegacy.kt | 3 + .../utils/WalletWarningsAnalyticsSender.kt | 1 + .../domain/GetMultiWalletWarningsFactory.kt | 118 +++--- .../wallet/state/model/WalletNotification.kt | 10 +- .../wallet/state/model/WalletState.kt | 7 + .../transformers/AddWalletTransformer.kt | 2 + .../InitializeWalletsTransformer.kt | 2 + .../ReinitializeNewWalletTransformer.kt | 2 + .../ReinitializeWalletTransformer.kt | 2 + .../transformers/SetTokenListTransformer.kt | 19 + .../TangemPayRefreshNeededStateTransformer.kt | 1 - ...TangemPayRefreshShowProgressTransformer.kt | 12 +- .../transformers/UnlockWalletTransformer.kt | 2 + .../converter/TangemPayMainBlockConverter.kt | 110 ++++++ .../state/utils/WalletLoadingStateFactory.kt | 6 + .../presentation/wallet/ui/WalletScreen.kt | 52 ++- .../components/common/WalletNotifications.kt | 13 + .../visa/TangemPayMainScreenBlock.kt | 38 +- .../components/visa/TangemPayRefreshBlock.kt | 1 - 59 files changed, 1679 insertions(+), 432 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/notifications/CreatePaymentAccountNotification.kt rename core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/{PaymentAccountStatusDM.kt => PaymentAccountStatusValueDM.kt} (52%) create mode 100644 core/ui/src/main/res/drawable/img_tangem_pay_visa_banner.webp delete mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt delete mode 100644 domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt create mode 100644 features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/component/TangemPayMainBlockComponent.kt create mode 100644 features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayMainUM.kt create mode 100644 features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/component/DefaultTangemPayMainBlockComponent.kt create mode 100644 features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayMainModule.kt create mode 100644 features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt index 6cc4b2bf7e..8c3453b855 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt @@ -8,23 +8,20 @@ import com.tangem.utils.converter.Converter class AccountIconItemStateConverter( val size: AccountIconSize = AccountIconSize.Default, -) : Converter { +) : Converter { - override fun convert(value: Account): CurrencyIconState.CryptoPortfolio = when (value) { - is Account.CryptoPortfolio -> when { - value.icon.value == CryptoPortfolioIcon.Icon.Letter -> CurrencyIconState.CryptoPortfolio.Letter( - char = value.accountName.toUM().value, - color = value.icon.color.getUiColor(), - isGrayscale = false, - size = size, - ) - else -> CurrencyIconState.CryptoPortfolio.Icon( - resId = value.icon.value.getResId(), - color = value.icon.color.getUiColor(), - isGrayscale = false, - size = size, - ) - } - is Account.Payment -> TODO("[REDACTED_JIRA]") + override fun convert(value: Account.CryptoPortfolio): CurrencyIconState.CryptoPortfolio = when { + value.icon.value == CryptoPortfolioIcon.Icon.Letter -> CurrencyIconState.CryptoPortfolio.Letter( + char = value.accountName.toUM().value, + color = value.icon.color.getUiColor(), + isGrayscale = false, + size = size, + ) + else -> CurrencyIconState.CryptoPortfolio.Icon( + resId = value.icon.value.getResId(), + color = value.icon.color.getUiColor(), + isGrayscale = false, + size = size, + ) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/CreatePaymentAccountNotification.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/CreatePaymentAccountNotification.kt new file mode 100644 index 0000000000..a681d0df23 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/CreatePaymentAccountNotification.kt @@ -0,0 +1,98 @@ +package com.tangem.common.ui.notifications + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.notifications.CloseableIconButton +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.ForceDarkTheme +import com.tangem.core.ui.res.TangemTheme + +private const val GRADIENT_START_COLOR = 0xFF252934 +private const val GRADIENT_END_COLOR = 0xFF12141E +private const val GRADIENT_OFFSET_X = 164f +private const val GRADIENT_OFFSET_Y = 39f +private const val GRADIENT_RADIUS = 82f + +@Composable +fun CreatePaymentAccountNotification( + onClick: () -> Unit, + onCloseClick: () -> Unit, + @DrawableRes image: Int, + title: TextReference, + subtitle: TextReference, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background( + brush = Brush.radialGradient( + colors = listOf(Color(GRADIENT_START_COLOR), Color(GRADIENT_END_COLOR)), + center = Offset(GRADIENT_OFFSET_X, GRADIENT_OFFSET_Y), + radius = GRADIENT_RADIUS, + ), + ) + .clickable(onClick = onClick), + ) { + Image( + modifier = Modifier.size(78.dp), + painter = painterResource(id = image), + contentDescription = null, + ) + Column( + modifier = Modifier + .padding(start = 78.dp, top = 12.dp, end = 12.dp, bottom = 12.dp) + .align(Alignment.CenterStart), + ) { + Text( + modifier = Modifier.padding(end = TangemTheme.dimens.size32), + text = title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.constantWhite, + ) + Text( + text = subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + CloseableIconButton( + onClick = onCloseClick, + modifier = Modifier.align(alignment = Alignment.TopEnd), + iconTint = TangemTheme.colors.icon.inactive, + ) + } +} + +@Preview(widthDp = 360) +@Composable +private fun CreatePaymentAccountNotification_Preview() { + ForceDarkTheme { + CreatePaymentAccountNotification( + onClick = {}, + onCloseClick = {}, + image = R.drawable.img_tangem_pay_visa_banner, + title = resourceReference(R.string.tangempay_onboarding_banner_title), + subtitle = resourceReference(R.string.tangempay_onboarding_banner_description), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index de63c7bbcd..effeecade9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -9,7 +9,7 @@ import com.tangem.common.json.MoshiJsonConverter import com.tangem.datasource.api.common.adapter.* import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.datasource.local.network.entity.NetworkStatusDM -import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM import com.tangem.datasource.utils.SerializeNullsFactory import com.tangem.domain.models.scan.serialization.* import dagger.Module @@ -47,13 +47,12 @@ class MoshiModule { .withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"), ) .add( - NamePolymorphicAdapterFactory.of(PaymentAccountStatusDM::class.java) - .withSubtype(PaymentAccountStatusDM.NotCreated::class.java, "not_created") - .withSubtype(PaymentAccountStatusDM.UnderReview::class.java, "kyc_status") - .withSubtype(PaymentAccountStatusDM.IssuingCard::class.java, "issuing_card") - .withSubtype(PaymentAccountStatusDM.Locked::class.java, "locked") - .withSubtype(PaymentAccountStatusDM.Loaded::class.java, "balance") - .withSubtype(PaymentAccountStatusDM.CardIssueFailed::class.java, "card_issue_failed"), + NamePolymorphicAdapterFactory.of(PaymentAccountStatusValueDM::class.java) + .withSubtype(PaymentAccountStatusValueDM.NotCreated::class.java, "not_created") + .withSubtype(PaymentAccountStatusValueDM.UnderReview::class.java, "kyc_status") + .withSubtype(PaymentAccountStatusValueDM.IssuingCard::class.java, "issuing_card") + .withSubtype(PaymentAccountStatusValueDM.ActiveCard::class.java, "active_card") + .withSubtype(PaymentAccountStatusValueDM.CardIssueFailed::class.java, "card_issue_failed"), ) .add( PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc") diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt similarity index 52% rename from core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt index 589fb4d915..7b1ca88f44 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt @@ -11,43 +11,58 @@ import java.math.BigDecimal /** * Payment account status for storage in the local cache. * - * @see [com.tangem.domain.pay.PaymentAccountStatus] + * @see [com.tangem.domain.models.account.AccountStatus.Payment] */ @JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER) -sealed interface PaymentAccountStatusDM { +sealed interface PaymentAccountStatusValueDM { @NameLabel("not_created") data class NotCreated( @Json(name = "not_created") val marker: Boolean = true, - ) : PaymentAccountStatusDM + ) : PaymentAccountStatusValueDM @NameLabel("kyc_status") data class UnderReview( @Json(name = "kyc_status") val kycStatus: KycStatus, - ) : PaymentAccountStatusDM + @Json(name = "customer_id") val customerId: String, + ) : PaymentAccountStatusValueDM @NameLabel("issuing_card") data class IssuingCard( @Json(name = "issuing_card") val marker: Boolean = true, - ) : PaymentAccountStatusDM + ) : PaymentAccountStatusValueDM - @NameLabel("locked") - data class Locked( - @Json(name = "locked") val marker: Boolean = true, - ) : PaymentAccountStatusDM - - @NameLabel("balance") - data class Loaded( + @NameLabel("active_card") + data class ActiveCard( + @Json(name = "active_card") val isLocked: Boolean, + @Json(name = "customer_id") val customerId: String, @Json(name = "card_id") val cardId: String, @Json(name = "last_four_digits") val lastFourDigits: String, - @Json(name = "balance") val balance: BigDecimal, @Json(name = "currency_code") val currencyCode: String, @Json(name = "deposit_address") val depositAddress: String?, @Json(name = "is_pin_set") val isPinSet: Boolean, - ) : PaymentAccountStatusDM + @Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM, + @Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM, + ) : PaymentAccountStatusValueDM @NameLabel("card_issue_failed") data class CardIssueFailed( @Json(name = "card_issue_failed") val marker: Boolean = true, - ) : PaymentAccountStatusDM + @Json(name = "customer_id") val customerId: String, + ) : PaymentAccountStatusValueDM + + @JsonClass(generateAdapter = true) + data class FiatBalanceDM( + @Json(name = "available_balance") val availableBalance: BigDecimal, + @Json(name = "currency") val currency: String, + ) + + @JsonClass(generateAdapter = true) + data class CryptoBalanceDM( + @Json(name = "id") val id: String, + @Json(name = "chain_id") val chainId: Long, + @Json(name = "deposit_address") val depositAddress: String, + @Json(name = "token_contract_address") val tokenContractAddress: String, + @Json(name = "balance") val balance: BigDecimal, + ) } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/img_tangem_pay_visa_banner.webp b/core/ui/src/main/res/drawable/img_tangem_pay_visa_banner.webp new file mode 100644 index 0000000000000000000000000000000000000000..5768fd6820284ab1721fdeb6cdce23e26767e9ad GIT binary patch literal 3402 zcmV-Q4Yl%8Nk&FO4FCXFMM6+kP&il$0000G0000t0RT4v06|PpNY(%V00D4S+ip}L zp%Nki36X$=IsgfEfP_duLM0#}5-K5oZ|q8$Z$v0^+qPl%xFzi)y3JPLKll&+ga1x0 zG{nfvJ<+DZbFr7DsOFp(k(5%5!*+>cpY2CQb$t^>CJIeA+Q;RmQOqK9N>hw2jiOkc zrKrA$VxvCn*{UzC`mjfh_bB=7dmPY7*ak7duJ6?m{|m57sMYJc8aK!(-lcXMZztzCWD_LjUQTcw$b)3ma6 zt^Qn|Y1sk98Hcv4{QF@6K0Pr?ZG1b2?50KR3DTI zOf^=vX6-Y%cH+S_?NC@>&@SFNuD>vVsSMILOep1e+4}fq+fZ}*e|h}CR3O?O2=!4t zHkL+Y#h9n41^U!uS(%s)vQS(+ar_Jqm}v0eRMCP*6h4^XVRrQ5KN$JQ@y+V6j#r)B z+4`(%O+E-mHct?~6CYb`5m((7h-bT#an~}7SED1>BKN;mRh8kZ=RU6^)MXR5@{)_m z^gI{YPcv4cPf6hshSOWo)Z(*M(qMji7YoND_~qeBT(S3z1Sv z`CGfYxwIj9+MR)jp7|EaYsMw0tyDi>lR$jlJLRgQvKvkXr2l+kgH99d+p5ve!obj) ze!fqvta4#Pq(9p&lQ7F{p0!m~QWbiCj_9TTAU8!;`jyOeEoe01MV z!J+Q-l4}Ia@`|+%D4R`hlCE`+(MS>7+jOm zJM=?4zaq}+O|tc5VCr;(Qe`Uj2qb-dix>}P_?C$=j_I z_UA$B5FlwO|MVsj~XqS$tI^f$AeUOkHh6s#W|~o2(4C=@}@i`!}Vk*Iw>^$ zP^)jPHWI}|<+3Yr3hrG^+0C;5Fm<{<|ibX(4 zpn#Mhg_$^i5ui!QszzRGxc@6>E4_bWl4oJ8Q12}f0^9^2hRdUwG`%NwqdS$IM;yj2 z*?^=W{od$=GigC2O?S$T4aya4z#G%CHR;gpO5dhHS-YyLm5!cZNS}S&o zH|M8PnY-6&?CLH#yZ2~(f-n64;9}dl7*!^1;wPZs>ixrpjbO|m5W%CById#%^gW+u z2OwO96OC)09sH7iwRItZ84 zBb}Az8hREp7P-$J>#@|2f5b)5O^S++WYf#aw``dCsCQ`fN_4;Zhuk&~!v5nQtWAYV z8gosBa6n7dTM}w!IW2|42L0BUif#sxtdE4EPHwc*ejrK9Fr&Mn#WA(-Sh*vh%z7f! z-yjy)4lGKQ|EF%jYY3k~-PkBz6$KPY0S-e*1;dUjM zrK*d=GgrQPk5gIU>Gg?+I~KkY`ANaCPV}=Qr?j1l)UJ9xd+E(P-0Z_iA*6{m(02sE zLG<3->c;-%l@S)u(^rgq{*n|YWcpK?rNd;)OLM1n8pK!o5eNq5La=5rSK8|PuTsSE zyPcozzITmTUCO;G<&(fMA6xl2NnV$CU8@;WKT z_rzyL1H+og-B^v+-Nc~1RWe(MXI8;a=DfggOk*9DYFHw8Q$ zH5L1@yLL|tkRV^VQ+!#$Ys9}}nI@(Z>IlWP=Etprl?2=-YF+tH9(+JN03StytOI9j z@-bSHBw}Dv8<$|a@u?dG=3pBfpe3@iex3ws^|#G_2-QKFDU+cW+CK@hry#=n*umG@ z9za_UzVUos2RzbG7iSZ}GeQ}?5}T@@19Nzk^G%1K1CORBxZ6jI2j|m?v*2vhZ%6#^ z_wAa4O==!k9$kbo_i}1hs?{~&L@w`IUOt&))S~A5QeYNe(M|Pmj$gjaWJXvC{sCeA zUZRv9j!{YWzxO+p8xA~EXDbesKAgn+RHff(WZJb~q!cs6vibgyWcY#mx+n~>NEfSW z2le?lop~qfPj@xbrw#IhFDh{vzM0M4wAOHQ2Ye>91mYXAQGxS#Z0F-xG+I*ASSrYYUD6(G6$DDT5Gz+THvQ-a4~~2bDo#RZlZxR9E!n{d z@)l4?m11-Gz}7S=Az*gV{hVzJ3p`g1Sgbhka1htg+lBOQs=B;J`69k(tuCc zFP{{AFU#3}2E+5%y?ex~U0L=~S@XmzAw57Mw{`}r|r{s@#+l4C1)ypzZk zT*GnaLLF`sI_ShU#fg`hDUlgPwPo|&Ue_v_H7h0aYcA{#hso?1tyB8-vVkcav(>62v8d(CJ_ zehbu-9(zPN(nyssD*f|jCf>lh|DEbB)3kT*y>>WvQ-!o`s)X9W7To&Kf7|Ys<{KHT zWHrB@BS#%!r#WDWdw3D*&ESLC`b`Q!G+8k1MLpEzN5YJz1|GOPM3$``Mr35zqiPLA?dCkwo=V!**c%d@r>@6Ti(Bw*#%W7mz1 zubGgOOU@6Che;LF=Uv56_}H~qlA?SfI8f(^8N5P!(&6fJ-y=ma)Mx;e{Avn4c8h^X g^6Voz=J0g>T&C5~f}kMVu~@{rd-&df5c~iD0IiawMgRZ+ literal 0 HcmV?d00001 diff --git a/data/account/build.gradle.kts b/data/account/build.gradle.kts index 61664f301e..9f3fa0d8a4 100644 --- a/data/account/build.gradle.kts +++ b/data/account/build.gradle.kts @@ -33,8 +33,11 @@ dependencies { api(projects.domain.models) api(projects.domain.tokens) api(projects.domain.wallets) + api(projects.domain.visa) // endregion + implementation(projects.features.tangempay.details.api) // Remove after TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED + // region Project - Data implementation(projects.data.common) // endregion @@ -47,6 +50,7 @@ dependencies { // region Tangem dependencies implementation(tangemDeps.card.core) implementation(tangemDeps.blockchain) + implementation(tangemDeps.hot.core) // endregion // region DI diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt index 8eb7e90e0d..28fd5fd6be 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt @@ -1,10 +1,18 @@ package com.tangem.data.account.producer import arrow.core.Option +import arrow.core.getOrElse import arrow.core.none +import com.tangem.common.card.FirmwareVersion import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.SingleAccountListProducer +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.core.flow.FlowProducerTools +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.features.tangempay.TangemPayFeatureToggles +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -12,6 +20,7 @@ import dagger.assisted.AssistedInject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map /** * Default implementation of [SingleAccountListProducer]. @@ -27,6 +36,8 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor( @Assisted val params: SingleAccountListProducer.Params, override val flowProducerTools: FlowProducerTools, private val walletAccountListFlowFactory: WalletAccountListFlowFactory, + private val tangemPayFeatureToggles: TangemPayFeatureToggles, + private val userWalletsListRepository: UserWalletsListRepository, private val dispatchers: CoroutineDispatcherProvider, ) : SingleAccountListProducer { @@ -34,8 +45,32 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor( @OptIn(ExperimentalCoroutinesApi::class) override fun produce(): Flow { - return walletAccountListFlowFactory.create(userWalletId = params.userWalletId) - .flowOn(dispatchers.default) + val accountListFlow: Flow = if (tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled) { + combineWithPaymentAccount() + } else { + walletAccountListFlowFactory.create(userWalletId = params.userWalletId) + } + + return accountListFlow.flowOn(dispatchers.default) + } + + private fun combineWithPaymentAccount(): Flow { + return walletAccountListFlowFactory.create(params.userWalletId) + .map { accountList -> + val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) + if (userWallet.isPaymentAccountSupported()) { + accountList.plus(Account.Payment(params.userWalletId)).getOrElse { throwable -> + error("Can not combine account list and payment account status: $throwable") + } + } else { + accountList + } + } + } + + private fun UserWallet.isPaymentAccountSupported(): Boolean = when (this) { + is UserWallet.Cold -> scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable + is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword } @AssistedFactory diff --git a/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt index b1069afb7c..4837e4f2a6 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt @@ -3,10 +3,12 @@ package com.tangem.data.account.producer import com.google.common.truth.Truth import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.SingleAccountListProducer +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.test.core.getEmittedValues import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* @@ -29,6 +31,10 @@ class DefaultSingleAccountListProducerTest { private val userWalletId = UserWalletId("011") private val flowProducerTools: FlowProducerTools = mockk() + private val tangemPayFeatureToggles = mockk { + every { this@mockk.isTangemPayAccountsRefactorEnabled } returns false + } + private val userWalletsListRepository = mockk() private val userWallet = mockk { every { this@mockk.walletId } returns userWalletId } @@ -38,6 +44,8 @@ class DefaultSingleAccountListProducerTest { walletAccountListFlowFactory = walletAccountListFlowFactory, dispatchers = TestingCoroutineDispatcherProvider(), flowProducerTools = flowProducerTools, + tangemPayFeatureToggles = tangemPayFeatureToggles, + userWalletsListRepository = userWalletsListRepository, ) @AfterEach diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt deleted file mode 100644 index db6fbcca4f..0000000000 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.tangem.data.pay.converter - -import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter.convert -import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter.convertBack -import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM -import com.tangem.domain.models.StatusSource -import com.tangem.domain.pay.PaymentAccountStatus -import com.tangem.utils.converter.TwoWayConverter - -/** - * Two-way converter between [PaymentAccountStatus] and [PaymentAccountStatusDM]. - * - * [convert] maps domain → data model. Returns null for transient statuses that should not be persisted - * (Loading, ExposedDevice, Unavailable, NotSynced). - * - * [convertBack] maps data model → domain. All restored statuses have [StatusSource.CACHE] as source. - */ -internal object PaymentAccountStatusDMConverter : - TwoWayConverter { - - override fun convert(value: PaymentAccountStatus): PaymentAccountStatusDM? { - return when (value) { - is PaymentAccountStatus.NotCreated -> PaymentAccountStatusDM.NotCreated() - is PaymentAccountStatus.UnderReview -> PaymentAccountStatusDM.UnderReview(kycStatus = value.kycStatus) - is PaymentAccountStatus.IssuingCard -> PaymentAccountStatusDM.IssuingCard() - is PaymentAccountStatus.Locked -> PaymentAccountStatusDM.Locked() - is PaymentAccountStatus.Loaded -> PaymentAccountStatusDM.Loaded( - cardId = value.cardId, - lastFourDigits = value.lastFourDigits, - balance = value.balance, - currencyCode = value.currencyCode, - depositAddress = value.depositAddress, - isPinSet = value.isPinSet, - ) - is PaymentAccountStatus.Error.CardIssueFailed -> PaymentAccountStatusDM.CardIssueFailed() - // Transient statuses are not persisted - is PaymentAccountStatus.Loading, - is PaymentAccountStatus.Error.ExposedDevice, - is PaymentAccountStatus.Error.Unavailable, - is PaymentAccountStatus.Error.NotSynced, - -> null - } - } - - override fun convertBack(value: PaymentAccountStatusDM?): PaymentAccountStatus { - return when (value) { - is PaymentAccountStatusDM.CardIssueFailed -> PaymentAccountStatus.Error.CardIssueFailed - is PaymentAccountStatusDM.NotCreated -> PaymentAccountStatus.NotCreated - is PaymentAccountStatusDM.IssuingCard -> PaymentAccountStatus.IssuingCard(source = StatusSource.CACHE) - is PaymentAccountStatusDM.Locked -> PaymentAccountStatus.Locked(source = StatusSource.CACHE) - is PaymentAccountStatusDM.UnderReview -> PaymentAccountStatus.UnderReview( - source = StatusSource.CACHE, - kycStatus = value.kycStatus, - ) - is PaymentAccountStatusDM.Loaded -> PaymentAccountStatus.Loaded( - source = StatusSource.CACHE, - cardId = value.cardId, - lastFourDigits = value.lastFourDigits, - balance = value.balance, - currencyCode = value.currencyCode, - depositAddress = value.depositAddress, - isPinSet = value.isPinSet, - ) - null -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.CACHE) - } - } -} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt new file mode 100644 index 0000000000..a42852436a --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt @@ -0,0 +1,139 @@ +package com.tangem.data.pay.converter + +import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convert +import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convertBack +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.utils.converter.TwoWayConverter + +/** + * Two-way converter between [PaymentAccountStatusValue] and [PaymentAccountStatusValueDM]. + * + * [convert] maps domain → data model. Returns null for transient statuses that should not be persisted + * (Loading, ExposedDevice, Unavailable, NotSynced). + * + * [convertBack] maps data model → domain. All restored statuses have [StatusSource.CACHE] as source. + */ +internal object PaymentAccountStatusValueDMConverter : + TwoWayConverter { + + override fun convert(value: PaymentAccountStatusValue): PaymentAccountStatusValueDM? { + return when (value) { + is PaymentAccountStatusValue.NotCreated -> PaymentAccountStatusValueDM.NotCreated() + is PaymentAccountStatusValue.UnderReview -> PaymentAccountStatusValueDM.UnderReview( + kycStatus = value.kycStatus, + customerId = value.customerId, + ) + is PaymentAccountStatusValue.IssuingCard -> PaymentAccountStatusValueDM.IssuingCard() + is PaymentAccountStatusValue.Locked -> PaymentAccountStatusValueDM.ActiveCard( + isLocked = true, + customerId = value.customerId, + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + fiatBalance = value.fiatBalance.toDM(), + cryptoBalance = value.cryptoBalance.toDM(), + ) + is PaymentAccountStatusValue.Loaded -> PaymentAccountStatusValueDM.ActiveCard( + isLocked = false, + customerId = value.customerId, + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + fiatBalance = value.fiatBalance.toDM(), + cryptoBalance = value.cryptoBalance.toDM(), + ) + is PaymentAccountStatusValue.Error.CardIssueFailed -> PaymentAccountStatusValueDM.CardIssueFailed( + customerId = value.customerId, + ) + // Transient statuses are not persisted + is PaymentAccountStatusValue.Loading, + is PaymentAccountStatusValue.Error.ExposedDevice, + is PaymentAccountStatusValue.Error.Unavailable, + is PaymentAccountStatusValue.Error.NotSynced, + -> null + } + } + + override fun convertBack(value: PaymentAccountStatusValueDM?): PaymentAccountStatusValue { + return when (value) { + is PaymentAccountStatusValueDM.NotCreated -> PaymentAccountStatusValue.NotCreated + is PaymentAccountStatusValueDM.CardIssueFailed -> PaymentAccountStatusValue.Error.CardIssueFailed( + customerId = value.customerId, + ) + is PaymentAccountStatusValueDM.IssuingCard -> PaymentAccountStatusValue.IssuingCard( + source = StatusSource.CACHE, + ) + is PaymentAccountStatusValueDM.ActiveCard -> if (value.isLocked) { + PaymentAccountStatusValue.Locked( + source = StatusSource.CACHE, + customerId = value.customerId, + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + fiatBalance = value.fiatBalance.toDomain(), + cryptoBalance = value.cryptoBalance.toDomain(), + ) + } else { + PaymentAccountStatusValue.Loaded( + source = StatusSource.CACHE, + customerId = value.customerId, + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + fiatBalance = value.fiatBalance.toDomain(), + cryptoBalance = value.cryptoBalance.toDomain(), + ) + } + is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview( + source = StatusSource.CACHE, + kycStatus = value.kycStatus, + customerId = value.customerId, + ) + null -> PaymentAccountStatusValue.Error.Unavailable + } + } + + private fun PaymentAccountStatusValue.FiatBalance.toDM(): PaymentAccountStatusValueDM.FiatBalanceDM { + return PaymentAccountStatusValueDM.FiatBalanceDM( + availableBalance = availableBalance, + currency = currency, + ) + } + + private fun PaymentAccountStatusValue.CryptoBalance.toDM(): PaymentAccountStatusValueDM.CryptoBalanceDM { + return PaymentAccountStatusValueDM.CryptoBalanceDM( + id = id, + chainId = chainId, + depositAddress = depositAddress, + tokenContractAddress = tokenContractAddress, + balance = balance, + ) + } + + private fun PaymentAccountStatusValueDM.FiatBalanceDM.toDomain(): PaymentAccountStatusValue.FiatBalance { + return PaymentAccountStatusValue.FiatBalance( + availableBalance = availableBalance, + currency = currency, + ) + } + + private fun PaymentAccountStatusValueDM.CryptoBalanceDM.toDomain(): PaymentAccountStatusValue.CryptoBalance { + return PaymentAccountStatusValue.CryptoBalance( + id = id, + chainId = chainId, + depositAddress = depositAddress, + tokenContractAddress = tokenContractAddress, + balance = balance, + ) + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index c1842bbec1..3887f44e6f 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -15,10 +15,9 @@ import com.tangem.data.pay.usecase.DefaultGetTangemPayCustomerIdUseCase import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.mapWithStringKeyTypes -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher @@ -32,6 +31,7 @@ import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Binds import dagger.Module @@ -118,7 +118,7 @@ internal interface TangemPayDataModule { persistenceDataStore = DataStoreFactory.create( serializer = MoshiDataStoreSerializer( moshi = moshi, - types = mapWithStringKeyTypes(), + types = mapWithStringKeyTypes(), defaultValue = emptyMap(), ), produceFile = { context.dataStoreFile(fileName = "payment_account_statuses") }, diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index 7342c3da2a..57cc801313 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -2,17 +2,19 @@ package com.tangem.data.pay.flow import arrow.core.Either import com.tangem.data.pay.store.PaymentAccountStatusesStore -import com.tangem.domain.core.utils.eitherOn +import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.kyc.KycStatus -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.PaymentAccountStatus import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.visa.error.VisaApiError +import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.security.isSecurityExposed import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -29,87 +31,101 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, ) : PaymentAccountStatusFetcher { + private val logger = TangemLogger.withTag(TAG) + override suspend fun invoke(params: PaymentAccountStatusFetcher.Params): Either = - eitherOn(dispatchers.default) { - TangemLogger.withTag(TAG).i("fetch: ${params.userWalletId.stringValue}") + Either.catchOn(dispatchers.default) { + val account = Account.Payment(userWalletId = params.userWalletId) + logger.i("fetch: ${params.userWalletId.stringValue}") if (deviceSecurity.isSecurityExposed()) { - TangemLogger.withTag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") - TangemLogger.withTag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") - TangemLogger.withTag( - TAG, - ).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") + logger.i("fetch security info: rooted: ${deviceSecurity.isRooted}") + logger.i("fetch security info: xposed: ${deviceSecurity.isXposed}") + logger.i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") - return@eitherOn paymentAccountStatusesStore.store( + return@catchOn paymentAccountStatusesStore.store( userWalletId = params.userWalletId, - status = PaymentAccountStatus.Error.ExposedDevice, + status = AccountStatus.Payment( + account = account, + value = PaymentAccountStatusValue.Error.ExposedDevice, + ), ) } val status = onboardingRepository.hasTangemPayInWallet(userWalletId = params.userWalletId) .fold( ifLeft = { error -> - TangemLogger.withTag( - TAG, - ).e("Failed check wallet ${params.userWalletId}: ${error.javaClass.simpleName}") + logger.e("Failed check wallet ${params.userWalletId}: ${error.javaClass.simpleName}") when (error) { - is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated - else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + is VisaApiError.NotPaeraCustomer -> PaymentAccountStatusValue.NotCreated + else -> PaymentAccountStatusValue.Error.Unavailable } }, ifRight = { hasTangemPay -> - proceedHasTangemPayResult(userWalletId = params.userWalletId, hasTangemPay = hasTangemPay) + proceedHasTangemPayResult(account = account, hasTangemPay = hasTangemPay) }, ) - TangemLogger.withTag(TAG).i("invoke status ${params.userWalletId}: $status") - paymentAccountStatusesStore.store(userWalletId = params.userWalletId, status = status) + logger.i("invoke status ${params.userWalletId}: $status") + paymentAccountStatusesStore.store( + userWalletId = params.userWalletId, + status = AccountStatus.Payment(account = account, value = status), + ) + }.onLeft { + paymentAccountStatusesStore.updateStatusSource( + userWalletId = params.userWalletId, + source = StatusSource.ONLY_CACHE, + ) } private suspend fun proceedHasTangemPayResult( - userWalletId: UserWalletId, + account: Account.Payment, hasTangemPay: Boolean, - ): PaymentAccountStatus { - TangemLogger.withTag(TAG).i("proceedHasTangemPayResult for $userWalletId hasTangemPay: $hasTangemPay") + ): PaymentAccountStatusValue { + logger.i("proceedHasTangemPayResult for ${account.userWalletId} hasTangemPay: $hasTangemPay") return if (hasTangemPay) { - fetchTangemPayAccountStatus(userWalletId = userWalletId) + fetchTangemPayAccountStatus(account) } else { - PaymentAccountStatus.NotCreated + PaymentAccountStatusValue.NotCreated } } - private suspend fun fetchTangemPayAccountStatus(userWalletId: UserWalletId): PaymentAccountStatus { - val prevResult = paymentAccountStatusesStore.getSyncOrNull(userWalletId) - if (prevResult == null || prevResult is PaymentAccountStatus.Error) { - paymentAccountStatusesStore.store(userWalletId = userWalletId, status = PaymentAccountStatus.Loading) + private suspend fun fetchTangemPayAccountStatus(account: Account.Payment): PaymentAccountStatusValue { + val prevResult = paymentAccountStatusesStore.getSyncOrNull(account.userWalletId) + if (prevResult == null || prevResult.value is PaymentAccountStatusValue.Error) { + paymentAccountStatusesStore.store( + userWalletId = account.userWalletId, + status = AccountStatus.Payment(account = account, value = PaymentAccountStatusValue.Loading), + ) } - return proceedWithOrderId(userWalletId = userWalletId) + return proceedWithOrderId(account = account) } - private suspend fun proceedWithOrderId(userWalletId: UserWalletId): PaymentAccountStatus { - return if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) { - PaymentAccountStatus.Error.NotSynced + private suspend fun proceedWithOrderId(account: Account.Payment): PaymentAccountStatusValue { + return if (!onboardingRepository.isTangemPayInitialDataProduced(account.userWalletId)) { + PaymentAccountStatusValue.Error.NotSynced } else { - val orderId = onboardingRepository.getOrderId(userWalletId) + val orderId = onboardingRepository.getOrderId(account.userWalletId) if (orderId != null) { - proceedWithOrderId(userWalletId = userWalletId, orderId = orderId) + proceedWithOrderId(account = account, orderId = orderId) } else { - proceedWithoutOrder(userWalletId = userWalletId) + proceedWithoutOrder(account = account) } } } - private suspend fun proceedWithoutOrder(userWalletId: UserWalletId): PaymentAccountStatus { - return onboardingRepository.getCustomerInfo(userWalletId).fold( + private suspend fun proceedWithoutOrder(account: Account.Payment): PaymentAccountStatusValue { + return onboardingRepository.getCustomerInfo(account.userWalletId).fold( ifLeft = { error -> - TangemLogger.withTag(TAG).e("proceedWithoutOrder $userWalletId error: $error") + logger.e("proceedWithoutOrder ${account.userWalletId} error: $error") error.mapToPaymentAccountStatus() }, ifRight = { customerInfo -> - TangemLogger.withTag(TAG).i("proceedWithoutOrder data customerInfo $userWalletId") + logger.i("proceedWithoutOrder data customerInfo ${account.userWalletId}") val status = customerInfo.mapToPaymentAccountStatus() - if (customerInfo.productInstance == null) { - onboardingRepository.createOrder(userWalletId) + if (status is PaymentAccountStatusValue.IssuingCard && customerInfo.kycStatus == KycStatus.APPROVED) { + // If order id wasn't saved -> start order creation and get customer info + onboardingRepository.createOrder(account.userWalletId) .onLeft { TangemLogger.withTag(TAG).e("createOrder failed: $it") } } status @@ -117,63 +133,94 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( ) } - private suspend fun proceedWithOrderId(userWalletId: UserWalletId, orderId: String): PaymentAccountStatus { - return customerOrderRepository.getOrderData(userWalletId, orderId = orderId).fold( + private suspend fun proceedWithOrderId(account: Account.Payment, orderId: String): PaymentAccountStatusValue { + return customerOrderRepository.getOrderData(userWalletId = account.userWalletId, orderId = orderId).fold( ifLeft = { error -> - TangemLogger.withTag(TAG).e("proceedWithOrderId $userWalletId orderId: $orderId error: $error") + logger.e("proceedWithOrderId ${account.userWalletId} orderId: $orderId error: $error") error.mapToPaymentAccountStatus() }, ifRight = { orderData -> - TangemLogger.withTag(TAG).i("proceedWithOrderId $userWalletId: $orderId status: ${orderData.status}") + logger.i("proceedWithOrderId $account.userWalletId: $orderId status: ${orderData.status}") when (orderData.status) { // Kyc is passed and user waits for order creation -> no need to get customer info OrderStatus.NEW, OrderStatus.PROCESSING, - -> PaymentAccountStatus.IssuingCard(source = StatusSource.ACTUAL) + -> PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL) OrderStatus.CANCELED -> { - PaymentAccountStatus.Error.CardIssueFailed + PaymentAccountStatusValue.Error.CardIssueFailed(customerId = orderData.customerId) } OrderStatus.COMPLETED -> { // Order was completed -> clear order id and get customer info - onboardingRepository.clearOrderId(userWalletId) - onboardingRepository.getCustomerInfo(userWalletId = userWalletId) + onboardingRepository.clearOrderId(account.userWalletId) + onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId) .fold( ifLeft = { it.mapToPaymentAccountStatus() }, ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() }, ) } - OrderStatus.UNKNOWN -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + OrderStatus.UNKNOWN -> PaymentAccountStatusValue.Error.Unavailable } }, ) } - private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatus { + private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatusValue { val cardInfo = this.cardInfo val productInstance = this.productInstance return if (kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty()) { - PaymentAccountStatus.UnderReview(source = StatusSource.ACTUAL, kycStatus = kycStatus) - } else if (cardInfo != null && productInstance != null) { - PaymentAccountStatus.Loaded( + PaymentAccountStatusValue.UnderReview( source = StatusSource.ACTUAL, - cardId = productInstance.cardId, - lastFourDigits = cardInfo.lastFourDigits, - balance = cardInfo.balance, - currencyCode = cardInfo.currencyCode, - depositAddress = cardInfo.depositAddress, - isPinSet = cardInfo.isPinSet, + kycStatus = kycStatus, + customerId = requireNotNull(customerId) { "CustomerId must not be null" }, + ) + } else if (cardInfo != null && productInstance != null && !customerId.isNullOrEmpty()) { + convertToContentState( + productInstance = productInstance, + cardInfo = cardInfo, + customerId = requireNotNull(customerId) { "CustomerId must not be null" }, ) } else { - PaymentAccountStatus.IssuingCard(source = StatusSource.ACTUAL) + PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL) } } - private fun VisaApiError.mapToPaymentAccountStatus(): PaymentAccountStatus { + private fun convertToContentState( + productInstance: CustomerInfo.ProductInstance, + cardInfo: CustomerInfo.CardInfo, + customerId: String, + ): PaymentAccountStatusValue { + return when (productInstance.frozenState) { + TangemPayCardFrozenState.Frozen -> PaymentAccountStatusValue.Locked( + source = StatusSource.ACTUAL, + customerId = customerId, + cardId = productInstance.cardId, + lastFourDigits = cardInfo.lastFourDigits, + currencyCode = cardInfo.currencyCode, + depositAddress = cardInfo.depositAddress, + isPinSet = cardInfo.isPinSet, + fiatBalance = cardInfo.fiatBalance, + cryptoBalance = cardInfo.cryptoBalance, + ) + else -> PaymentAccountStatusValue.Loaded( + source = StatusSource.ACTUAL, + customerId = customerId, + cardId = productInstance.cardId, + lastFourDigits = cardInfo.lastFourDigits, + currencyCode = cardInfo.currencyCode, + depositAddress = cardInfo.depositAddress, + isPinSet = cardInfo.isPinSet, + fiatBalance = cardInfo.fiatBalance, + cryptoBalance = cardInfo.cryptoBalance, + ) + } + } + + private fun VisaApiError.mapToPaymentAccountStatus(): PaymentAccountStatusValue { return when (this) { - is VisaApiError.RefreshTokenExpired -> PaymentAccountStatus.Error.NotSynced - is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated - else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced + is VisaApiError.NotPaeraCustomer -> PaymentAccountStatusValue.NotCreated + else -> PaymentAccountStatusValue.Error.Unavailable } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt index cb021c1ee8..c2c53ae7cd 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt @@ -4,8 +4,9 @@ import arrow.core.Option import arrow.core.some import com.tangem.data.pay.store.PaymentAccountStatusesStore import com.tangem.domain.core.flow.FlowProducerTools -import com.tangem.domain.models.StatusSource -import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.pay.flow.PaymentAccountStatusProducer import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted @@ -21,12 +22,15 @@ internal class DefaultPaymentAccountStatusProducer @AssistedInject constructor( private val paymentAccountStatusesStore: PaymentAccountStatusesStore, private val dispatchers: CoroutineDispatcherProvider, ) : PaymentAccountStatusProducer { - override val fallback: Option - get() = PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL).some() - override fun produce(): Flow { + private val account = Account.Payment(userWalletId = params.userWalletId) + + override val fallback: Option + get() = AccountStatus.Payment(account = account, value = PaymentAccountStatusValue.Error.Unavailable).some() + + override fun produce(): Flow { return paymentAccountStatusesStore.get(userWalletId = params.userWalletId) - .onEmpty { emit(value = PaymentAccountStatus.NotCreated) } + .onEmpty { emit(value = AccountStatus.Payment(account, PaymentAccountStatusValue.NotCreated)) } .flowOn(dispatchers.default) } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 8e20d83b04..6880483883 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -13,6 +13,7 @@ import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.TangemPayEligibilityType +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -139,6 +140,7 @@ internal class DefaultOnboardingRepository @Inject constructor( ?: error("no userWallet found") } + @Suppress("ComplexCondition") private suspend fun getCustomerInfo( userWalletId: UserWalletId, response: CustomerMeResponse.Result?, @@ -148,14 +150,26 @@ internal class DefaultOnboardingRepository @Inject constructor( val card = response?.card val fiatBalance = response?.balance?.fiat + val cryptoBalance = response?.balance?.crypto val paymentAccount = response?.paymentAccount - val cardInfo = if (paymentAccount != null && card != null && fiatBalance != null) { + val cardInfo = if (paymentAccount != null && card != null && fiatBalance != null && cryptoBalance != null) { CardInfo( lastFourDigits = card.cardNumberEnd, balance = fiatBalance.availableBalance, currencyCode = fiatBalance.currency, depositAddress = response.depositAddress, isPinSet = response.card?.isPinSet == true, + fiatBalance = PaymentAccountStatusValue.FiatBalance( + availableBalance = fiatBalance.availableBalance, + currency = fiatBalance.currency, + ), + cryptoBalance = PaymentAccountStatusValue.CryptoBalance( + id = cryptoBalance.id, + chainId = cryptoBalance.chainId.toLong(), + depositAddress = cryptoBalance.depositAddress.orEmpty(), + tokenContractAddress = cryptoBalance.tokenContractAddress, + balance = cryptoBalance.balance, + ), ) } else { null @@ -167,7 +181,7 @@ internal class DefaultOnboardingRepository @Inject constructor( } cardFrozenStateStore.store(key = instance.cardId, value = cardFrozenState) - ProductInstance(id = instance.id, cardId = instance.cardId) + ProductInstance(id = instance.id, cardId = instance.cardId, frozenState = cardFrozenState) } return CustomerInfo( customerId = response?.id, diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt index d42e7623a1..ec8a48d880 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt @@ -1,11 +1,14 @@ package com.tangem.data.pay.store import androidx.datastore.core.DataStore -import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter +import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.PaymentAccountStatus import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.coroutineScope @@ -14,8 +17,8 @@ import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch -internal typealias WalletIdWithPaymentStatus = Map -internal typealias WalletIdWithPaymentStatusDM = Map +internal typealias WalletIdWithPaymentStatus = Map +internal typealias WalletIdWithPaymentStatusDM = Map /** * Store for payment account statuses with dual storage (runtime + persistence). @@ -26,7 +29,7 @@ internal typealias WalletIdWithPaymentStatusDM = Map, private val persistenceDataStore: DataStore, - private val scope: AppCoroutineScope, + scope: AppCoroutineScope, ) { init { @@ -34,8 +37,10 @@ internal class PaymentAccountStatusesStore( try { val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch runtimeStore.store( - value = cachedStatuses.mapValues { (_, statusDM) -> - PaymentAccountStatusDMConverter.convertBack(statusDM) + value = cachedStatuses.mapValues { (rawUserWalletId, statusDM) -> + val account = Account.Payment(userWalletId = UserWalletId(rawUserWalletId)) + val statusValue = PaymentAccountStatusValueDMConverter.convertBack(value = statusDM) + AccountStatus.Payment(account = account, value = statusValue) }, ) } catch (e: Exception) { @@ -44,18 +49,28 @@ internal class PaymentAccountStatusesStore( } } - fun get(userWalletId: UserWalletId): Flow { + fun get(userWalletId: UserWalletId): Flow { return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] } } - suspend fun getSyncOrNull(userWalletId: UserWalletId): PaymentAccountStatus? { + suspend fun getSyncOrNull(userWalletId: UserWalletId): AccountStatus.Payment? { return runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue) } - suspend fun store(userWalletId: UserWalletId, status: PaymentAccountStatus) { + suspend fun updateStatusSource(userWalletId: UserWalletId, source: StatusSource) { + runtimeStore.update(emptyMap()) { stored -> + stored.toMutableMap().apply { + val paymentAccountStatus = this[userWalletId.stringValue] ?: return@update stored + val newValue = paymentAccountStatus.copy(value = paymentAccountStatus.value.copySealed(source = source)) + put(key = userWalletId.stringValue, value = newValue) + } + } + } + + suspend fun store(userWalletId: UserWalletId, status: AccountStatus.Payment) { coroutineScope { launch { storeInRuntime(userWalletId = userWalletId, status = status) } - launch { storeInPersistence(userWalletId = userWalletId, status = status) } + launch { storeInPersistence(userWalletId = userWalletId, status = status.value) } } } @@ -63,7 +78,7 @@ internal class PaymentAccountStatusesStore( return runtimeStore.getSyncOrDefault(emptyMap()).containsKey(userWalletId.stringValue) } - private suspend fun storeInRuntime(userWalletId: UserWalletId, status: PaymentAccountStatus) { + private suspend fun storeInRuntime(userWalletId: UserWalletId, status: AccountStatus.Payment) { runtimeStore.update(default = emptyMap()) { stored -> stored.toMutableMap().apply { put(key = userWalletId.stringValue, value = status) @@ -71,8 +86,8 @@ internal class PaymentAccountStatusesStore( } } - private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatus) { - val statusDM = PaymentAccountStatusDMConverter.convert(value = status) ?: return + private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatusValue) { + val statusDM = PaymentAccountStatusValueDMConverter.convert(value = status) ?: return persistenceDataStore.updateData { storedStatuses -> storedStatuses.toMutableMap().apply { put(key = userWalletId.stringValue, value = statusDM) diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index c413c7e46f..ab66eca0fd 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -108,16 +108,14 @@ data class AccountList private constructor( } fun flattenMapCurrencies(): Map = buildMap { - accounts.forEach { acc -> - val account = when (acc) { - is Account.CryptoPortfolio -> acc - is Account.Payment -> TODO("[REDACTED_JIRA]") + accounts + .filterIsInstance() + .forEach { account -> + account.cryptoCurrencies.forEach { currency -> + val key = account.accountId to currency.id + put(key, currency) + } } - account.cryptoCurrencies.forEach { currency -> - val key = account.accountId to currency.id - put(key, currency) - } - } } /** diff --git a/domain/account/status/build.gradle.kts b/domain/account/status/build.gradle.kts index 7c6c759fa8..20d40f33d4 100644 --- a/domain/account/status/build.gradle.kts +++ b/domain/account/status/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { api(projects.domain.staking) api(projects.domain.tokens) api(projects.domain.tokens.models) + api(projects.domain.visa) api(projects.domain.walletManager) api(projects.domain.wallets) @@ -39,6 +40,8 @@ dependencies { implementation(deps.kotlin.serialization) implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) + implementation(tangemDeps.hot.core) // region DI implementation(deps.hilt.android) diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt index 5250d458de..1c341528d0 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt @@ -3,6 +3,7 @@ package com.tangem.domain.account.status.producer import arrow.core.Option import arrow.core.none import arrow.core.toOption +import com.tangem.common.card.FirmwareVersion import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.domain.account.models.AccountCurrencyId import com.tangem.domain.account.models.AccountList @@ -33,6 +34,7 @@ import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.networks.multi.MultiNetworkStatusProducer import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.repository.NetworksRepository +import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.quotes.multi.MultiQuoteStatusSupplier import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiStakingBalanceProducer @@ -42,6 +44,7 @@ import com.tangem.domain.tokens.operations.CryptoCurrencyStatusFactory import com.tangem.domain.tokens.operations.PriceChangeCalculator import com.tangem.domain.tokens.operations.TokenListFactory import com.tangem.domain.tokens.operations.TotalFiatBalanceCalculator +import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -69,6 +72,7 @@ import java.math.BigDecimal * [REDACTED_AUTHOR] */ +// TODO: Move to :data:account:status [REDACTED_JIRA] @Suppress("LongParameterList") @OptIn(ExperimentalCoroutinesApi::class) internal class DefaultSingleAccountStatusListProducer @AssistedInject constructor( @@ -76,6 +80,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo override val flowProducerTools: FlowProducerTools, private val userWalletsListRepository: UserWalletsListRepository, private val singleAccountListSupplier: SingleAccountListSupplier, + private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier, private val networksRepository: NetworksRepository, private val dispatchers: CoroutineDispatcherProvider, private val networkStatusSupplier: MultiNetworkStatusSupplier, @@ -116,31 +121,52 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo flattenCurrency = flattenCurrency, ) - combine( + if (userWallet.isPaymentAccountSupported()) { + combineWithPaymentAccount( + accountListFlow = accountListFlow, + cryptoCurrencyStatusFlow = cryptoCurrencyStatusFlow, + paymentAccountStatusFlow = paymentAccountStatusSupplier.invoke(userWalletId = params.userWalletId), + ) + } else { + combineWithoutPaymentAccount( + accountListFlow = accountListFlow, + cryptoCurrencyStatusFlow = cryptoCurrencyStatusFlow, + ) + } + .collect { accountStatusList -> channel.send(accountStatusList) } + } + + private fun combineWithPaymentAccount( + accountListFlow: StateFlow, + cryptoCurrencyStatusFlow: Flow>, + paymentAccountStatusFlow: Flow, + ): Flow { + return combine( flow = accountListFlow, flow2 = cryptoCurrencyStatusFlow, - transform = { accountList, currencyStatusMap -> - val accountStatuses: List = accountList.accounts.map { acc -> - val account: Account.CryptoPortfolio = when (acc) { - is Account.CryptoPortfolio -> acc - is Account.Payment -> TODO("[REDACTED_JIRA]") - } - if (account.cryptoCurrencies.isEmpty()) { - account.toEmptyAccountStatus() - } else { - val statuses: List = account.cryptoCurrencies.map { currency -> - val acId = account.accountId to currency.id - currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus() + flow3 = paymentAccountStatusFlow, + transform = { accountList, currencyStatusMap, paymentAccountStatus -> + val accountStatuses = accountList.accounts.map { account -> + when (account) { + is Account.Payment -> paymentAccountStatus + is Account.CryptoPortfolio -> if (account.cryptoCurrencies.isEmpty()) { + account.toEmptyAccountStatus() + } else { + val statuses: List = + account.cryptoCurrencies.map { currency -> + val acId = account.accountId to currency.id + currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus() + } + AccountStatus.CryptoPortfolio( + account = account, + tokenList = TokenListFactory.create( + statuses = statuses, + groupType = accountList.groupType, + sortType = accountList.sortType, + ), + priceChangeLce = PriceChangeCalculator.calculate(statuses = statuses), + ) } - AccountStatus.CryptoPortfolio( - account = account, - tokenList = TokenListFactory.create( - statuses = statuses, - groupType = accountList.groupType, - sortType = accountList.sortType, - ), - priceChangeLce = PriceChangeCalculator.calculate(statuses = statuses), - ) } } val balances = accountStatuses.flattenTotalFiatBalance() @@ -156,7 +182,58 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo ) }, ) - .collect { accountStatusList -> channel.send(accountStatusList) } + } + + private fun combineWithoutPaymentAccount( + accountListFlow: StateFlow, + cryptoCurrencyStatusFlow: Flow>, + ): Flow { + return combine( + flow = accountListFlow, + flow2 = cryptoCurrencyStatusFlow, + transform = { accountList, currencyStatusMap -> + val accountStatuses = accountList.accounts + .filterIsInstance() + .map { account -> + when (account) { + is Account.CryptoPortfolio -> if (account.cryptoCurrencies.isEmpty()) { + account.toEmptyAccountStatus() + } else { + val statuses: List = + account.cryptoCurrencies.map { currency -> + val acId = account.accountId to currency.id + currencyStatusMap[acId] ?: currency.toLoadingCurrencyStatus() + } + AccountStatus.CryptoPortfolio( + account = account, + tokenList = TokenListFactory.create( + statuses = statuses, + groupType = accountList.groupType, + sortType = accountList.sortType, + ), + priceChangeLce = PriceChangeCalculator.calculate(statuses = statuses), + ) + } + } + } + val balances = accountStatuses.flattenTotalFiatBalance() + + AccountStatusList( + userWalletId = accountList.userWalletId, + accountStatuses = accountStatuses, + totalAccounts = accountList.totalAccounts, + totalFiatBalance = TotalFiatBalanceCalculator.calculate(balances), + totalArchivedAccounts = accountList.totalArchivedAccounts, + sortType = accountList.sortType, + groupType = accountList.groupType, + ) + }, + ) + } + + private fun UserWallet.isPaymentAccountSupported(): Boolean = when (this) { + is UserWallet.Cold -> scanResponse.card.firmwareVersion >= FirmwareVersion.HDWalletAvailable + is UserWallet.Hot -> hotWalletId.authType != HotWalletId.AuthType.NoPassword } private fun ProducerScope.flattenCurrencyStatusFlow( @@ -278,7 +355,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo return map { accountStatus -> when (accountStatus) { is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance - is AccountStatus.Payment -> accountStatus.totalFiatBalance + is AccountStatus.Payment -> accountStatus.value.totalFiatBalance } } } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt index a0442452ef..79ab7d3f5a 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -178,12 +178,15 @@ sealed interface Account { @Serializable data class Payment( override val accountId: AccountId, - override val accountName: AccountName, - val cryptoCurrencies: List, ) : Account { + override val accountName: AccountName.Custom = AccountName.Custom("Payment").getOrElse { + error("Can not create account name for Payment account with userWalletId = ${accountId.userWalletId}") + } - init { - error("Not yet implemented") + companion object { + operator fun invoke(userWalletId: UserWalletId): Payment { + return Payment(accountId = AccountId.forPaymentAccount(userWalletId = userWalletId)) + } } } } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt index 31df2e7281..748f8de199 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt @@ -1,7 +1,6 @@ package com.tangem.domain.models.account import com.tangem.domain.core.lce.Lce -import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.quote.PriceChange import com.tangem.domain.models.tokenlist.TokenList @@ -43,7 +42,7 @@ sealed interface AccountStatus { @Serializable data class Payment( override val account: Account.Payment, - val totalFiatBalance: TotalFiatBalance, + val value: PaymentAccountStatusValue, ) : AccountStatus } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt new file mode 100644 index 0000000000..fc44ccc105 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt @@ -0,0 +1,193 @@ +package com.tangem.domain.models.account + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.models.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable + +/** + * Represents the various states a payment account can have, encapsulating different information based on the state. + * + * @property source The source of the status information. + */ +@Serializable +sealed class PaymentAccountStatusValue { + abstract val source: StatusSource + + /** The total fiat balance associated with this status. */ + val totalFiatBalance: TotalFiatBalance + get() = when (this) { + is Error, + is IssuingCard, + is NotCreated, + is UnderReview, + -> TotalFiatBalance.Loaded(amount = SerializedBigDecimal.ZERO, source = source) + is Loading -> TotalFiatBalance.Loading + is Locked -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source) + is Loaded -> TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance, source = source) + } + + /** + * Copies the status with a new [source]. + * + * @param source The new source of the status information. + */ + fun copySealed(source: StatusSource): PaymentAccountStatusValue { + return when (this) { + is IssuingCard -> copy(source = source) + is Loaded -> copy(source = source) + is Locked -> copy(source = source) + is UnderReview -> copy(source = source) + is Loading, + is NotCreated, + is Error, + -> this + } + } + + /** Represents the Loading state of a payment account, typically while fetching its details. */ + @Serializable + data object Loading : PaymentAccountStatusValue() { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** Represents a state where the payment account has not been created yet. */ + @Serializable + data object NotCreated : PaymentAccountStatusValue() { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** + * Represents a state where the payment account is under review (KYC). + * + * @property source The source of the status information. + * @property kycStatus The current KYC status. + * @property customerId The unique identifier of the customer. + */ + @Serializable + data class UnderReview( + override val source: StatusSource, + val kycStatus: KycStatus, + val customerId: String, + ) : PaymentAccountStatusValue() + + /** + * Represents a state where the card for the payment account is being issued. + * + * @property source The source of the status information. + */ + @Serializable + data class IssuingCard(override val source: StatusSource) : PaymentAccountStatusValue() + + /** + * Represents a state where the payment account is locked. + * + * @property source The source of the status information. + * @property customerId The unique identifier of the customer. + * @property cardId The unique identifier of the card. + * @property lastFourDigits The last four digits of the card number. + * @property currencyCode The code of the currency. + * @property depositAddress The address for deposits, if available. + * @property isPinSet Indicates if the PIN is set for the card. + * @property fiatBalance The fiat balance details. + * @property cryptoBalance The crypto balance details. + */ + @Serializable + data class Locked( + override val source: StatusSource, + val customerId: String, + val cardId: String, + val lastFourDigits: String, + val currencyCode: String, + val depositAddress: String?, + val isPinSet: Boolean, + val fiatBalance: FiatBalance, + val cryptoBalance: CryptoBalance, + ) : PaymentAccountStatusValue() + + /** + * Represents a state where the payment account is successfully loaded with complete information. + * + * @property source The source of the status information. + * @property customerId The unique identifier of the customer. + * @property cardId The unique identifier of the card. + * @property lastFourDigits The last four digits of the card number. + * @property currencyCode The code of the currency. + * @property depositAddress The address for deposits, if available. + * @property isPinSet Indicates if the PIN is set for the card. + * @property fiatBalance The fiat balance details. + * @property cryptoBalance The crypto balance details. + */ + @Serializable + data class Loaded( + override val source: StatusSource, + val customerId: String, + val cardId: String, + val lastFourDigits: String, + val currencyCode: String, + val depositAddress: String?, + val isPinSet: Boolean, + val fiatBalance: FiatBalance, + val cryptoBalance: CryptoBalance, + ) : PaymentAccountStatusValue() + + /** Represents an error state for the payment account status. */ + @Serializable + sealed class Error : PaymentAccountStatusValue() { + /** Error state indicating the device is exposed. */ + @Serializable + data object ExposedDevice : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** Error state indicating the account is unavailable. */ + @Serializable + data object Unavailable : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** Error state indicating the account data is not synced. */ + @Serializable + data object NotSynced : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + + /** + * Error state indicating that card issuance failed. + * + * @property customerId The unique identifier of the customer. + */ + @Serializable + data class CardIssueFailed(val customerId: String) : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + } + + /** + * Represents the fiat balance of the payment account. + * + * @property availableBalance The amount of available balance in fiat. + * @property currency The currency of the balance. + */ + @Serializable + data class FiatBalance(val availableBalance: SerializedBigDecimal, val currency: String) + + /** + * Represents the crypto balance of the payment account. + * + * @property id The unique identifier of the crypto asset. + * @property chainId The identifier of the blockchain network. + * @property depositAddress The address for deposits. + * @property tokenContractAddress The contract address of the token. + * @property balance The amount of the crypto balance. + */ + @Serializable + data class CryptoBalance( + val id: String, + val chainId: Long, + val depositAddress: String, + val tokenContractAddress: String, + val balance: SerializedBigDecimal, + ) +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt deleted file mode 100644 index bdc604087f..0000000000 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.domain.pay - -import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.kyc.KycStatus -import com.tangem.domain.models.serialization.SerializedBigDecimal -import kotlinx.serialization.Serializable - -@Serializable -sealed class PaymentAccountStatus { - - abstract val source: StatusSource - - @Serializable - data object Loading : PaymentAccountStatus() { - override val source: StatusSource = StatusSource.ACTUAL - } - - @Serializable - data object NotCreated : PaymentAccountStatus() { - override val source: StatusSource = StatusSource.ACTUAL - } - - @Serializable - data class UnderReview( - override val source: StatusSource, - val kycStatus: KycStatus, - ) : PaymentAccountStatus() - - @Serializable - data class IssuingCard(override val source: StatusSource) : PaymentAccountStatus() - - @Serializable - data class Locked(override val source: StatusSource) : PaymentAccountStatus() - - @Serializable - data class Loaded( - override val source: StatusSource, - val cardId: String, - val lastFourDigits: String, - val balance: SerializedBigDecimal, - val currencyCode: String, - val depositAddress: String?, - val isPinSet: Boolean, - ) : PaymentAccountStatus() - - @Serializable - sealed class Error : PaymentAccountStatus() { - @Serializable - data object ExposedDevice : Error() { - override val source: StatusSource = StatusSource.ACTUAL - } - - @Serializable - data class Unavailable(override val source: StatusSource) : Error() - - @Serializable - data object NotSynced : Error() { - override val source: StatusSource = StatusSource.ACTUAL - } - - @Serializable - data object CardIssueFailed : Error() { - override val source: StatusSource = StatusSource.ACTUAL - } - } -} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt index c49a25f45d..313e5741ce 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt @@ -1,10 +1,10 @@ package com.tangem.domain.pay.flow import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.PaymentAccountStatus -interface PaymentAccountStatusProducer : FlowProducer { +interface PaymentAccountStatusProducer : FlowProducer { data class Params(val userWalletId: UserWalletId) interface Factory : FlowProducer.Factory diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt index 94580d26e1..11744a357c 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt @@ -1,10 +1,18 @@ package com.tangem.domain.pay.flow import com.tangem.domain.core.flow.FlowCachingSupplier -import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow @Suppress("UnnecessaryAbstractClass") abstract class PaymentAccountStatusSupplier( override val factory: PaymentAccountStatusProducer.Factory, override val keyCreator: (PaymentAccountStatusProducer.Params) -> String, -) : FlowCachingSupplier() \ No newline at end of file +) : FlowCachingSupplier() { + + operator fun invoke(userWalletId: UserWalletId): Flow { + val params = PaymentAccountStatusProducer.Params(userWalletId) + return this.invoke(params) + } +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index 2fd02e7a45..11ef7e59c7 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -1,6 +1,8 @@ package com.tangem.domain.pay.model +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.visa.model.TangemPayCardFrozenState import java.math.BigDecimal sealed class MainCustomerInfoContentState { @@ -25,6 +27,7 @@ data class CustomerInfo( data class ProductInstance( val id: String, val cardId: String, + val frozenState: TangemPayCardFrozenState, ) data class CardInfo( @@ -33,5 +36,7 @@ data class CustomerInfo( val currencyCode: String, val depositAddress: String?, val isPinSet: Boolean, + val fiatBalance: PaymentAccountStatusValue.FiatBalance, + val cryptoBalance: PaymentAccountStatusValue.CryptoBalance, ) } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 511457acb6..180b1b864f 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -15,8 +15,6 @@ import com.tangem.security.isSecurityExposed import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.* -private const val TAG = "TangemPayMainScreenCustomerInfoUseCase" - class TangemPayMainScreenCustomerInfoUseCase( private val onboardingRepository: OnboardingRepository, private val customerOrderRepository: CustomerOrderRepository, @@ -27,15 +25,15 @@ class TangemPayMainScreenCustomerInfoUseCase( val state: StateFlow>> field = MutableStateFlow(value = mapOf()) + private val logger = TangemLogger.withTag("TangemPayMainScreenCustomerInfoUseCase") + suspend fun fetch(userWalletId: UserWalletId) { - TangemLogger.withTag(TAG).i("fetch: ${userWalletId.stringValue}") + logger.i("fetch: ${userWalletId.stringValue}") if (deviceSecurity.isSecurityExposed()) { - TangemLogger.withTag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") - TangemLogger.withTag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") - TangemLogger.withTag( - TAG, - ).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") + logger.i("fetch security info: rooted: ${deviceSecurity.isRooted}") + logger.i("fetch security info: xposed: ${deviceSecurity.isXposed}") + logger.i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") updateState(userWalletId = userWalletId, either = TangemPayCustomerInfoError.ExposedDeviceError.left()) return // fast exit @@ -44,9 +42,7 @@ class TangemPayMainScreenCustomerInfoUseCase( onboardingRepository.hasTangemPayInWallet(userWalletId) .fold( ifLeft = { error -> - TangemLogger.withTag( - TAG, - ).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}") + logger.e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}") if (error is VisaApiError.NotPaeraCustomer) { showOnboardingBannerIfEligible(userWalletId) } else { @@ -54,7 +50,7 @@ class TangemPayMainScreenCustomerInfoUseCase( } }, ifRight = { hasTangemPay -> - TangemLogger.withTag(TAG).i("checkCustomerWallet for $userWalletId: $hasTangemPay") + logger.i("checkCustomerWallet for $userWalletId: $hasTangemPay") if (hasTangemPay) { val oldResult = state.value[userWalletId] if (oldResult == null) { @@ -129,11 +125,11 @@ class TangemPayMainScreenCustomerInfoUseCase( ): Either { return onboardingRepository.getCustomerInfo(userWalletId) .mapLeft { error -> - TangemLogger.withTag(TAG).e("mapErrorForCustomer: $error") + logger.e("mapErrorForCustomer: $error") error.mapErrorForCustomer() } .map { customerInfo -> - TangemLogger.withTag(TAG).i("customerInfo") + logger.i("customerInfo") if (customerInfo.productInstance == null) { onboardingRepository.createOrder(userWalletId) MainScreenCustomerInfo(info = customerInfo, orderStatus = OrderStatus.NEW) diff --git a/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt index d7e351ff96..b344d503d8 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt @@ -184,7 +184,7 @@ internal class PortfolioSelectorModel @Inject constructor( val account = accountStatus.account val accountBalance = when (accountStatus) { is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance - is AccountStatus.Payment -> accountStatus.totalFiatBalance + is AccountStatus.Payment -> accountStatus.value.totalFiatBalance } val accountItemUM = AccountPortfolioItemUMConverter( onClick = { selectorController.selectAccount(account.accountId) }, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index ddc4c9751a..5b62caa0f6 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -19,6 +19,7 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry @@ -105,7 +106,7 @@ internal class OnrampTokenListModel @Inject constructor( updateTokenListUM( SetLoadingAccountTokenListTransformer( appCurrency = appCurrency, - accountList = accountList.accountStatuses.toList(), + accountList = accountList.accountStatuses.filterCryptoPortfolio().toList(), isAccountsMode = isAccountsMode, ), ) @@ -237,6 +238,7 @@ internal class OnrampTokenListModel @Inject constructor( private fun AccountStatusList.filterAccountsByQuery( query: String, ): Map> = accountStatuses.asSequence() + .filterCryptoPortfolio() .associate { accountStatus -> when (accountStatus) { is AccountStatus.CryptoPortfolio -> { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 96f8ec12bf..14b10c2aad 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -31,6 +31,7 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network @@ -131,7 +132,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun getAccountCurrencyTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { val walletAccountCurrencyStatuses = singleAccountStatusListSupplier.getSyncOrNull( SingleAccountStatusListProducer.Params(userWalletId), - )?.accountStatuses.orEmpty() + )?.accountStatuses.orEmpty().filterCryptoPortfolio() val walletAccountCurrencyStatusesExceptInitial: Map> = walletAccountCurrencyStatuses.mapNotNull { accountStatus -> diff --git a/features/tangempay/main/api/build.gradle.kts b/features/tangempay/main/api/build.gradle.kts index 15fb515b8b..3ae2de80dd 100644 --- a/features/tangempay/main/api/build.gradle.kts +++ b/features/tangempay/main/api/build.gradle.kts @@ -15,4 +15,6 @@ dependencies { /** Compose */ implementation(deps.compose.runtime) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) } \ No newline at end of file diff --git a/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/component/TangemPayMainBlockComponent.kt b/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/component/TangemPayMainBlockComponent.kt new file mode 100644 index 0000000000..a38b7fca5d --- /dev/null +++ b/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/component/TangemPayMainBlockComponent.kt @@ -0,0 +1,19 @@ +package com.tangem.features.tangempay.component + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.features.tangempay.entity.TangemPayMainUM + +@Stable +interface TangemPayMainBlockComponent { + + fun LazyListScope.tangemPayMainContent( + state: TangemPayMainUM, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayMainUM.kt b/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayMainUM.kt new file mode 100644 index 0000000000..977ce0927e --- /dev/null +++ b/features/tangempay/main/api/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayMainUM.kt @@ -0,0 +1,26 @@ +package com.tangem.features.tangempay.entity + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +sealed class TangemPayMainUM { + + data object Empty : TangemPayMainUM() + data object Loading : TangemPayMainUM() + data class UnderReview(val subtitle: TextReference, val onClick: () -> Unit) : TangemPayMainUM() + data class IssuingCard(val onClick: () -> Unit) : TangemPayMainUM() + data class FailedToIssue(val onClick: () -> Unit) : TangemPayMainUM() + data class Content( + val subtitle: TextReference, + val isBalanceFlickering: Boolean, + val balance: TextReference, + val balanceSubtitle: TextReference, + val onClick: () -> Unit, + val shouldShowOnlyCacheWarning: Boolean, + ) : TangemPayMainUM() + + data object TemporaryUnavailable : TangemPayMainUM() + data object SyncNeeded : TangemPayMainUM() + data object ExposedDevice : TangemPayMainUM() +} \ No newline at end of file diff --git a/features/tangempay/main/impl/build.gradle.kts b/features/tangempay/main/impl/build.gradle.kts index eb442c8f69..c1aeff44a2 100644 --- a/features/tangempay/main/impl/build.gradle.kts +++ b/features/tangempay/main/impl/build.gradle.kts @@ -15,10 +15,9 @@ dependencies { /** Core */ implementation(projects.core.decompose) implementation(projects.core.ui) - implementation(projects.core.configToggles) /** Features api */ - implementation(projects.features.tangempay.details.api) + implementation(projects.features.tangempay.main.api) /** Compose */ implementation(deps.compose.foundation) diff --git a/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/component/DefaultTangemPayMainBlockComponent.kt b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/component/DefaultTangemPayMainBlockComponent.kt new file mode 100644 index 0000000000..716d22e5f7 --- /dev/null +++ b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/component/DefaultTangemPayMainBlockComponent.kt @@ -0,0 +1,37 @@ +package com.tangem.features.tangempay.component + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.features.tangempay.entity.TangemPayMainUM +import com.tangem.features.tangempay.ui.TangemPayMainBlockItem +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +private const val TANGEM_PAY_ACCOUNT_CONTENT_TYPE = "TangemPayAccount" + +@Suppress("UnusedPrivateProperty") +internal class DefaultTangemPayMainBlockComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted params: Unit, +) : TangemPayMainBlockComponent, AppComponentContext by context { + + override fun LazyListScope.tangemPayMainContent( + state: TangemPayMainUM, + isBalanceHidden: Boolean, + modifier: Modifier, + ) { + item( + key = TANGEM_PAY_ACCOUNT_CONTENT_TYPE, + contentType = TANGEM_PAY_ACCOUNT_CONTENT_TYPE, + ) { + TangemPayMainBlockItem(state, isBalanceHidden, modifier) + } + } + + @AssistedFactory + interface Factory : TangemPayMainBlockComponent.Factory { + override fun create(context: AppComponentContext, params: Unit): DefaultTangemPayMainBlockComponent + } +} \ No newline at end of file diff --git a/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayMainModule.kt b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayMainModule.kt new file mode 100644 index 0000000000..c7dc8b22ab --- /dev/null +++ b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayMainModule.kt @@ -0,0 +1,17 @@ +package com.tangem.features.tangempay.di + +import com.tangem.features.tangempay.component.DefaultTangemPayMainBlockComponent +import com.tangem.features.tangempay.component.TangemPayMainBlockComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal interface TangemPayMainModule { + @Binds + fun bindTangemPayMainBlockComponent( + factory: DefaultTangemPayMainBlockComponent.Factory, + ): TangemPayMainBlockComponent.Factory +} \ No newline at end of file diff --git a/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt new file mode 100644 index 0000000000..e95dcc262b --- /dev/null +++ b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt @@ -0,0 +1,355 @@ +package com.tangem.features.tangempay.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.inputrow.InputRowImageBase +import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.tangempay.entity.TangemPayMainUM +import com.tangem.features.tangempay.main.impl.R +import com.tangem.utils.StringsSigns.DASH_SIGN + +private const val DISABLED_ALPHA = 0.6F + +@Composable +internal fun TangemPayMainBlockItem(state: TangemPayMainUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { + when (state) { + is TangemPayMainUM.Empty -> Unit + is TangemPayMainUM.Loading -> TangemPayMainLoadingItem(modifier) + is TangemPayMainUM.UnderReview -> TangemPayMainUnderReviewItem(state, modifier) + is TangemPayMainUM.IssuingCard -> TangemPayMainIssuingCardItem(state, modifier) + is TangemPayMainUM.FailedToIssue -> TangemPayMainFailedIssueItem(state, modifier) + is TangemPayMainUM.Content -> TangemPayMainBlockContent(state, isBalanceHidden, modifier) + is TangemPayMainUM.TemporaryUnavailable -> TangemPayMainTempUnavailableItem(modifier) + is TangemPayMainUM.SyncNeeded -> TangemPayMainSyncNeededItem(modifier) + is TangemPayMainUM.ExposedDevice -> TangemPayMainExposedDeviceItem(modifier) + } +} + +@Composable +private fun TangemPayMainBlockContent( + state: TangemPayMainUM.Content, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier, + shape = TangemTheme.shapes.roundedCornersXMedium, + color = TangemTheme.colors.background.primary, + onClick = state.onClick, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + .padding(horizontal = 12.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Image( + painter = painterResource(R.drawable.img_visa_36), + contentDescription = null, + modifier = Modifier.size(36.dp), + ) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = stringResourceSafe(R.string.tangempay_payment_account), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = state.subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + Column( + modifier = Modifier.fillMaxHeight(), + verticalArrangement = Arrangement.spacedBy(2.dp), + horizontalAlignment = Alignment.End, + ) { + TangemPayFiatAmount( + text = state.balance.resolveReference(), + isBalanceFlickering = state.isBalanceFlickering, + isBalanceFromCache = state.shouldShowOnlyCacheWarning, + isBalanceHidden = isBalanceHidden, + ) + Text( + text = state.balanceSubtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.End, + ) + } + } + } +} + +@Composable +private fun TangemPayFiatAmount( + text: String, + isBalanceFlickering: Boolean, + isBalanceFromCache: Boolean, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + AnimatedVisibility(isBalanceFromCache) { + Row( + modifier = Modifier.padding(horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + modifier = Modifier.size(12.dp), + painter = painterResource(R.drawable.ic_error_sync_24), + tint = TangemTheme.colors.icon.inactive, + contentDescription = null, + ) + } + } + + Text( + text = text.orMaskWithStars(isBalanceHidden), + style = TangemTheme.typography.body2.applyBladeBrush( + isEnabled = isBalanceFlickering, + textColor = TangemTheme.colors.text.primary1, + ), + textAlign = TextAlign.End, + ) + } +} + +@Composable +private fun TangemPayMainUnderReviewItem(state: TangemPayMainUM.UnderReview, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + onClick = state.onClick, + ) { + InputRowImageBase( + modifier = Modifier + .padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = state.subtitle, + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + ) + } +} + +@Composable +private fun TangemPayMainTempUnavailableItem(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + enabled = false, + ) { + InputRowImageBase( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = TextReference.Str(DASH_SIGN), + subtitleColor = TangemTheme.colors.text.tertiary, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + ) + } +} + +@Composable +private fun TangemPayMainIssuingCardItem(state: TangemPayMainUM.IssuingCard, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + onClick = state.onClick, + ) { + InputRowImageBase( + modifier = Modifier + .padding(all = TangemTheme.dimens.spacing12), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = resourceReference(R.string.tangempay_issuing_your_card), + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + ) + } +} + +@Composable +private fun TangemPayMainFailedIssueItem(state: TangemPayMainUM.FailedToIssue, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + onClick = state.onClick, + ) { + InputRowImageBase( + modifier = Modifier + .padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = TextReference.Res(R.string.tangempay_payment_account), + caption = TextReference.Res(R.string.tangempay_failed_to_issue_card), + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = com.tangem.core.ui.R.drawable.img_visa_36, + iconEndRes = R.drawable.ic_alert_24, + endIconTint = TangemTheme.colors.icon.warning, + ) + } +} + +@Composable +private fun TangemPayMainSyncNeededItem(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + enabled = false, + ) { + InputRowImageBase( + modifier = Modifier.padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = resourceReference(R.string.tangempay_payment_account_sync_needed), + subtitleColor = TangemTheme.colors.text.tertiary, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + ) + } +} + +@Composable +private fun TangemPayMainExposedDeviceItem(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary) + .alpha(DISABLED_ALPHA), + enabled = false, + ) { + InputRowImageBase( + modifier = Modifier + .padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = resourceReference(R.string.tangem_pay_rooted_device_subtitle), + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + endIconTint = TangemTheme.colors.icon.warning, + ) + } +} + +@Composable +private fun TangemPayMainLoadingItem(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .background(color = TangemTheme.colors.background.primary, shape = TangemTheme.shapes.roundedCornersXMedium) + .padding(horizontal = 12.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CircleShimmer(modifier = Modifier.size(36.dp)) + Column( + modifier = Modifier.padding(start = 12.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + RectangleShimmer( + modifier = Modifier + .padding(vertical = 4.dp) + .sizeIn(minWidth = 70.dp, minHeight = 12.dp), + ) + RectangleShimmer( + modifier = Modifier + .padding(vertical = 2.dp) + .sizeIn(minWidth = 52.dp, minHeight = 12.dp), + ) + } + SpacerWMax() + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + RectangleShimmer( + modifier = Modifier + .padding(vertical = 4.dp) + .sizeIn(minWidth = 40.dp, minHeight = 12.dp), + ) + RectangleShimmer( + modifier = Modifier + .padding(vertical = 2.dp) + .sizeIn(minWidth = 40.dp, minHeight = 12.dp), + ) + } + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview +@Composable +private fun TangemPayMainItemsPreview( + @PreviewParameter(TangemPayMainUMPreviewParameterProvider::class) + state: TangemPayMainUM, +) { + TangemThemePreview { + TangemPayMainBlockItem(state = state, isBalanceHidden = false) + } +} + +private class TangemPayMainUMPreviewParameterProvider : CollectionPreviewParameterProvider( + collection = listOf( + TangemPayMainUM.Loading, + TangemPayMainUM.SyncNeeded, + TangemPayMainUM.TemporaryUnavailable, + TangemPayMainUM.ExposedDevice, + TangemPayMainUM.FailedToIssue(onClick = {}), + TangemPayMainUM.UnderReview(subtitle = resourceReference(R.string.tangempay_kyc_in_progress), onClick = {}), + TangemPayMainUM.IssuingCard(onClick = {}), + TangemPayMainUM.Content( + subtitle = TextReference.Str("*1234"), + isBalanceFlickering = true, + balance = TextReference.Str("$ 101.56"), + balanceSubtitle = TextReference.Str("USDC"), + onClick = {}, + shouldShowOnlyCacheWarning = true, + ), + ), +) \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 7fcdc3dc2e..e3f92c0e20 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -148,6 +148,7 @@ dependencies { implementation(projects.features.tangempay.details.api) implementation(projects.features.feed.api) implementation(projects.features.promoBanners.api) + implementation(projects.features.tangempay.main.api) /** Common modules */ implementation(projects.common) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index a62274f509..22f857a5f1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -37,6 +37,7 @@ import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.feed.entry.components.FeedEntryComponent import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.api.PushNotificationsParams +import com.tangem.features.tangempay.component.TangemPayMainBlockComponent import com.tangem.features.send.v2.api.NetworkSelectionComponent import com.tangem.features.tokenreceive.TokenReceiveComponent import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent @@ -51,6 +52,7 @@ internal class WalletComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted navigate: (WalletRoute) -> Unit, feedEntryComponentFactory: FeedEntryComponent.Factory, + tangemPayMainBlockComponentFactory: TangemPayMainBlockComponent.Factory, private val renameWalletComponentFactory: RenameWalletComponent.Factory, private val askBiometryComponentFactory: AskBiometryComponent.Factory, private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory, @@ -70,6 +72,12 @@ internal class WalletComponent @AssistedInject constructor( entryRoute = null, ) } + private val tangemPayMainBlockComponent by lazy { + tangemPayMainBlockComponentFactory.create( + context = child("tangemPayMainBlockComponent"), + params = Unit, + ) + } private val promoBannersBlockComponent: PromoBannersBlockComponent? by lazy { if (!newPromoBannersFeatureToggles.isNewPromoBannersEnabled) return@lazy null @@ -218,10 +226,11 @@ internal class WalletComponent @AssistedInject constructor( val bottomSheetState = remember { mutableStateOf(BottomSheetState.COLLAPSED) } var headerSize by remember { mutableStateOf(0.dp) } val dialog by dialog.subscribeAsState() + val uiState by model.uiState.collectAsStateWithLifecycle() if (designFeatureToggles.isRedesignEnabled) { WalletScreen2( - state = model.uiState.collectAsStateWithLifecycle().value, + state = uiState, bottomSheetContent = { BottomSheetContent( bottomSheetState = bottomSheetState, @@ -234,8 +243,9 @@ internal class WalletComponent @AssistedInject constructor( ) } else { WalletScreen( - state = model.uiState.collectAsStateWithLifecycle().value, + state = uiState, promoBannersBlockComponent = promoBannersBlockComponent, + tangemPayComponent = tangemPayMainBlockComponent, bottomSheetContent = { BottomSheetContent( bottomSheetState = bottomSheetState, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 1d3096be23..16d2963864 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -6,13 +6,13 @@ import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase @@ -25,20 +25,20 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.* import com.tangem.domain.notifications.GetIsHuaweiDeviceWithoutGoogleServicesUseCase import com.tangem.domain.notifications.repository.NotificationsRepository -import com.tangem.domain.qrscanning.models.QrResultSource -import com.tangem.domain.qrscanning.models.SourceType -import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase -import com.tangem.domain.qrscanning.models.ClassifiedQrContent -import com.tangem.domain.qrscanning.models.QrSendTarget -import com.tangem.domain.walletconnect.WcPairService -import com.tangem.domain.walletconnect.model.WcPairRequest +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase +import com.tangem.domain.qrscanning.models.ClassifiedQrContent +import com.tangem.domain.qrscanning.models.QrResultSource +import com.tangem.domain.qrscanning.models.QrSendTarget +import com.tangem.domain.qrscanning.models.SourceType +import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ResolveQrSendTargetsUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase +import com.tangem.domain.walletconnect.WcPairService +import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.wallets.usecase.* -import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.router.InnerWalletRouter @@ -61,6 +61,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejec import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.utils.Provider @@ -119,6 +120,8 @@ internal class WalletModel @Inject constructor( private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val wcPairService: WcPairService, private val resolveQrSendTargetsUseCase: ResolveQrSendTargetsUseCase, + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, + private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val uiMessageSender: UiMessageSender, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, @@ -427,14 +430,17 @@ internal class WalletModel @Inject constructor( updateTangemPayJobHolder.cancel() modelScope.launch { tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) while (isActive) { delay(TANGEM_PAY_UPDATE_INTERVAL) tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) } }.saveIn(updateTangemPayJobHolder) } else { // Don't refresh customer info periodically if the card was already issued, only update on swipe to refresh tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) } }.launchIn(modelScope) } @@ -543,6 +549,7 @@ internal class WalletModel @Inject constructor( walletImageResolver = walletImageResolver, isMainScreenQrScanningEnabled = walletFeatureToggles.isMainScreenQrScanningEnabled, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), ) @@ -589,6 +596,7 @@ internal class WalletModel @Inject constructor( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), ) } @@ -610,6 +618,7 @@ internal class WalletModel @Inject constructor( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), ) } @@ -624,6 +633,7 @@ internal class WalletModel @Inject constructor( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), ) @@ -685,6 +695,7 @@ internal class WalletModel @Inject constructor( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt index 1a2d2f0885..efa8fe4574 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt @@ -23,6 +23,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayDetailsConfig import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.model.TangemPayEntryPoint +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase @@ -30,7 +31,6 @@ import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHideOnboardingStateTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshNeededStateTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshShowProgressTransformer import kotlinx.coroutines.launch import javax.inject.Inject @@ -77,6 +77,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( private val tangemPayEligibilityManager: TangemPayEligibilityManager, private val uiMessageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, ) : BaseWalletClickIntents(), TangemPayIntents { override suspend fun onPullToRefresh() { @@ -85,20 +86,28 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( return } tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) } override fun onRefreshPayToken(userWallet: UserWallet) { - stateHolder.update(TangemPayRefreshShowProgressTransformer(userWallet.walletId)) + stateHolder.update( + TangemPayRefreshShowProgressTransformer( + userWalletId = userWallet.walletId, + shouldShowProgress = true, + ), + ) modelScope.launch { produceInitialDataTangemPay.invoke(userWallet.walletId) - .onRight { tangemPayMainScreenCustomerInfoUseCase.fetch(userWallet.walletId) } + .onRight { + tangemPayMainScreenCustomerInfoUseCase.fetch(userWallet.walletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWallet.walletId)) + } .onLeft { stateHolder.update( - transformer = TangemPayRefreshNeededStateTransformer( - userWallet = userWallet, + TangemPayRefreshShowProgressTransformer( userWalletId = userWallet.walletId, - onRefreshClick = { onRefreshPayToken(userWallet) }, + shouldShowProgress = false, ), ) } @@ -267,7 +276,10 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( analyticsEventHandler.send(TangemPayAnalyticsEvents.KycCancelled()) modelScope.launch { tangemPayOnboardingRepository.disableTangemPay(userWalletId) - .onRight { tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) } + .onRight { + tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) + paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) + } .onLeft { uiMessageSender.send(ToastMessage(resourceReference(R.string.common_something_went_wrong))) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt index 5485587450..91a230aa1f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt @@ -18,6 +18,7 @@ import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy.topBarConfig import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.features.tangempay.entity.TangemPayMainUM import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList @@ -217,6 +218,8 @@ internal object WalletScreenPreviewDataLegacy { onClick = {}, ), type = WalletType.Cold, + tangemPayMainUM = TangemPayMainUM.Empty, + isTangemPayRefactorEnabled = false, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 63e329b73f..09f8eba96e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -123,6 +123,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Warning.TangemPayUnreachable -> null is WalletNotification.UpgradeHotWalletPromo -> null is WalletNotification.TokenSyncCompleted -> null + is WalletNotification.CreateTangemPayAccount -> null } } 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 2438cafa5a..e81d00263d 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,10 +7,10 @@ import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.core.lce.Lce import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase @@ -18,6 +18,8 @@ import com.tangem.domain.hotwallet.GetUpgradeBannerClosureTimestampUseCase import com.tangem.domain.hotwallet.ShouldShowUpgradeHotWalletBannerUseCase import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -25,7 +27,6 @@ import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.promo.ShouldShowPromoWalletUseCase import com.tangem.domain.promo.models.PromoId import com.tangem.domain.settings.IsReadyToShowRateAppUseCase -import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase @@ -45,7 +46,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map - import javax.inject.Inject @Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") @@ -69,13 +69,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod", "CastNullableToNonNullableType") fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver - - val accountStatusListFlow by lazy { - val params = SingleAccountStatusListProducer.Params(userWallet.walletId) - accountDependencies.singleAccountStatusListSupplier(params) - .map { it.totalFiatBalance to it.flattenCurrencies() } - .map { Lce.Content(it) } - } + val params = SingleAccountStatusListProducer.Params(userWallet.walletId) + val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params) return combine( accountStatusListFlow, @@ -95,9 +90,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( .distinctUntilChanged(), ) { array -> array } .map { array -> - val lceTokens = array[0] as Lce>> - val totalFiatBalance = lceTokens.map { it.first } - val flattenCurrencies = lceTokens.map { it.second } + val accountStatusList = array[0] as AccountStatusList val isReadyToShowRating = array[1] as Boolean val isNeedToBackup = array[2] as Boolean val seedPhraseIssueStatus = array[3] as SeedPhraseNotificationsStatus @@ -108,8 +101,13 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val shouldShowUpgradeBanner = array[8] as Boolean val closureTimestamp = array[9] as? Long + val flattenCurrencies = accountStatusList.flattenCurrencies() + val paymentAccountStatus = accountStatusList.accountStatuses + .filterIsInstance() + .firstOrNull() + buildList { - addUsedOutdatedDataNotification(totalFiatBalance) + addUsedOutdatedDataNotification(accountStatusList.totalFiatBalance) addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) @@ -162,24 +160,54 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( if (!hasCriticalOrWarning) { addRateTheAppNotification(isReadyToShowRating, clickIntents) } + + // add as last warning + paymentAccountStatus?.let { paymentAccountStatus -> + addTangemPayWarnings( + status = paymentAccountStatus, + userWallet = userWallet, + walletClickIntents = clickIntents, + ) + } }.toImmutableList() } } - private fun MutableList.addUsedOutdatedDataNotification( - totalFiatBalance: Lce, + private fun MutableList.addTangemPayWarnings( + status: AccountStatus.Payment, + userWallet: UserWallet, + walletClickIntents: WalletClickIntents, ) { + val notification = when (status.value) { + is PaymentAccountStatusValue.Error.NotSynced -> WalletNotification.Warning.TangemPayRefreshNeeded( + buttonText = when (userWallet) { + is UserWallet.Cold -> resourceReference(id = R.string.home_button_scan) + is UserWallet.Hot -> resourceReference(id = R.string.tangempay_sync_needed_restore_access) + }, + onRefreshClick = { walletClickIntents.onRefreshPayToken(userWallet) }, + shouldShowProgress = false, + ) + is PaymentAccountStatusValue.NotCreated -> WalletNotification.CreateTangemPayAccount( + onClick = { walletClickIntents.onOnboardingBannerClick(userWallet.walletId) }, + onCloseClick = { walletClickIntents.onOnboardingBannerCloseClick(userWallet.walletId) }, + ) + is PaymentAccountStatusValue.Error.Unavailable -> WalletNotification.Warning.TangemPayUnreachable + is PaymentAccountStatusValue.Error.CardIssueFailed, + is PaymentAccountStatusValue.Error.ExposedDevice, + is PaymentAccountStatusValue.IssuingCard, + is PaymentAccountStatusValue.Loaded, + is PaymentAccountStatusValue.Loading, + is PaymentAccountStatusValue.Locked, + is PaymentAccountStatusValue.UnderReview, + -> null + } + notification?.let(::add) + } + + private fun MutableList.addUsedOutdatedDataNotification(totalFiatBalance: TotalFiatBalance) { addIf( element = WalletNotification.UsedOutdatedData, - condition = totalFiatBalance.fold( - ifLoading = { - (it as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE - }, - ifContent = { - (it as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE - }, - ifError = { false }, - ), + condition = (totalFiatBalance as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE, ) } @@ -254,7 +282,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addInformationalNotifications( userWallet: UserWallet, cardTypesResolver: CardTypesResolver?, - flattenCurrencies: Lce>, + flattenCurrencies: List, clickIntents: WalletClickIntents, ) { addIf( @@ -267,7 +295,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addMissingAddressesNotification( userWallet: UserWallet, - flattenCurrencies: Lce>, + flattenCurrencies: List, clickIntents: WalletClickIntents, ) { val currencies = flattenCurrencies.getMissingAddressCurrencies() @@ -285,10 +313,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun Lce>.getMissingAddressCurrencies(): List { - val flattenCurrencies = getOrNull(isPartialContentAccepted = true) ?: return emptyList() - - return flattenCurrencies + private fun List.getMissingAddressCurrencies(): List { + return this .filter { it.value is CryptoCurrencyStatus.MissedDerivation } .map(CryptoCurrencyStatus::currency) } @@ -330,7 +356,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addWarningNotifications( cardTypesResolver: CardTypesResolver?, - flattenCurrencies: Lce>, + flattenCurrencies: List, isNeedToBackup: Boolean, clickIntents: WalletClickIntents, ) { @@ -355,7 +381,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( } private fun MutableList.addCloreMigrationNotification( - flattenCurrencies: Lce>, + flattenCurrencies: List, clickIntents: WalletClickIntents, ) { val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return @@ -367,10 +393,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun Lce>.findCloreCurrency(): CryptoCurrencyStatus? { - val currencies = getOrNull(isPartialContentAccepted = true) ?: return null - - return currencies.find { currencyStatus -> + private fun List.findCloreCurrency(): CryptoCurrencyStatus? { + return this.find { currencyStatus -> BlockchainUtils.isClore(currencyStatus.currency.network.rawId) } } @@ -388,10 +412,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun Lce>.hasUnreachableNetworks(): Boolean { - val flattenCurrencies = getOrNull(isPartialContentAccepted = false) ?: return false - - return flattenCurrencies.any { it.value is CryptoCurrencyStatus.Unreachable } + private fun List.hasUnreachableNetworks(): Boolean { + return this.any { it.value is CryptoCurrencyStatus.Unreachable } } // Remove in first iteration of yield supply feature @@ -427,7 +449,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addFinishWalletActivationNotification( userWallet: UserWallet, - flattenCurrencies: Lce>, + flattenCurrencies: List, clickIntents: WalletClickIntents, shouldAccessCodeSkipped: Boolean, ) { @@ -437,12 +459,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val isAccessCodeRequired = userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword && !shouldAccessCodeSkipped val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired - - val type = flattenCurrencies.fold( - ifLoading = { return }, - ifContent = { it.getFinishWalletActivationType() }, - ifError = { WalletActivationBannerType.Attention }, - ) + val type = flattenCurrencies.getFinishWalletActivationType() addIf( element = WalletNotification.FinishWalletActivation( @@ -465,15 +482,14 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private suspend fun MutableList.addUpgradeHotWalletPromoNotification( userWallet: UserWallet, - flattenCurrencies: Lce>, + flattenCurrencies: List, clickIntents: WalletClickIntents, shouldShowUpgradeBanner: Boolean, closureTimestamp: Long?, ) { if (userWallet !is UserWallet.Hot) return - val currencies = flattenCurrencies.getOrNull(isPartialContentAccepted = true).orEmpty() - val hasBalance = currencies.any { it.value.amount.orZero().isPositive() } + val hasBalance = flattenCurrencies.any { it.value.amount.orZero().isPositive() } val shouldShow = checkHotWalletUpgradeBannerUseCase( walletId = userWallet.walletId, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index e3110857f1..77f9417e93 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -151,7 +151,6 @@ sealed class WalletNotification(val config: NotificationConfig) { ) data class TangemPayRefreshNeeded( - @DrawableRes private val tangemIcon: Int?, private val onRefreshClick: () -> Unit, private val buttonText: TextReference, private val shouldShowProgress: Boolean, @@ -160,7 +159,7 @@ sealed class WalletNotification(val config: NotificationConfig) { subtitle = resourceReference(id = R.string.tangempay_use_tangem_device_to_restore_payment_account), buttonsState = ButtonsState.PrimaryButtonConfig( text = buttonText, - iconResId = tangemIcon, + iconResId = R.drawable.ic_tangem_24, onClick = onRefreshClick, shouldShowProgress = shouldShowProgress, ), @@ -480,4 +479,11 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ), ) + + data class CreateTangemPayAccount(val onClick: () -> Unit, val onCloseClick: () -> Unit) : WalletNotification( + config = NotificationConfig( + subtitle = resourceReference(R.string.tangempay_onboarding_banner_description), + iconResId = R.drawable.img_tangem_pay_visa_banner, + ), + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 12261f3059..9459402f44 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -10,6 +10,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedTx import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedWalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder import com.tangem.feature.wallet.presentation.wallet.state.model.holder.WalletStateHolder +import com.tangem.features.tangempay.entity.TangemPayMainUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList @@ -24,6 +25,8 @@ internal sealed interface WalletState : WalletStateHolder { abstract val nftState: WalletNFTItemUM abstract val type: WalletType abstract val tangemPayState: TangemPayState + abstract val tangemPayMainUM: TangemPayMainUM + abstract val isTangemPayRefactorEnabled: Boolean // TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED data class Content( override val pullToRefreshConfig: PullToRefreshConfig, @@ -35,6 +38,8 @@ internal sealed interface WalletState : WalletStateHolder { override val nftState: WalletNFTItemUM, override val type: WalletType, override val tangemPayState: TangemPayState, + override val tangemPayMainUM: TangemPayMainUM, + override val isTangemPayRefactorEnabled: Boolean, ) : MultiCurrency() data class Locked( @@ -54,6 +59,8 @@ internal sealed interface WalletState : WalletStateHolder { override val tokensListState = WalletTokensListState.ContentState.Locked override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden override val tangemPayState: TangemPayState = TangemPayState.Empty + override val tangemPayMainUM: TangemPayMainUM = TangemPayMainUM.Empty + override val isTangemPayRefactorEnabled: Boolean = false } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt index ec3d6943be..7cba602ffb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt @@ -13,6 +13,7 @@ internal class AddWalletTransformer( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, + private val isTangemPayRefactorEnabled: Boolean, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { @@ -20,6 +21,7 @@ internal class AddWalletTransformer( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index a92b59ba44..4c669daeed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt @@ -28,6 +28,7 @@ internal class InitializeWalletsTransformer( private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, private val isMainScreenQrScanningEnabled: Boolean = false, + private val isTangemPayRefactorEnabled: Boolean, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { @@ -35,6 +36,7 @@ internal class InitializeWalletsTransformer( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt index 0c1b198d0c..ed1b3a1b10 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt @@ -24,6 +24,7 @@ internal class ReinitializeNewWalletTransformer( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, + private val isTangemPayRefactorEnabled: Boolean, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { @@ -31,6 +32,7 @@ internal class ReinitializeNewWalletTransformer( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt index 6dab085776..00a8977b27 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt @@ -20,6 +20,7 @@ internal class ReinitializeWalletTransformer( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, + private val isTangemPayRefactorEnabled: Boolean, ) : WalletStateTransformer(userWalletId = userWallet.walletId) { private val walletLoadingStateFactory by lazy { @@ -27,6 +28,7 @@ internal class ReinitializeWalletTransformer( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 4d5d813ddc..4c3d0689dc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.StakingAvailability @@ -8,10 +9,12 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletBalanceUMTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter +import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TangemPayMainBlockConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.WalletTokensListUMConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons import com.tangem.utils.logging.TangemLogger +import com.tangem.features.tangempay.entity.TangemPayMainUM import java.math.BigDecimal internal class SetTokenListTransformer( @@ -25,12 +28,17 @@ internal class SetTokenListTransformer( private val isAccountsModeEnabled: Boolean, ) : WalletStateTransformer(userWallet.walletId) { + private val tangemPayConverter by lazy { + TangemPayMainBlockConverter(tangemPayClickIntents = clickIntents) + } + override fun transform(prevState: WalletState): WalletState { return when (prevState) { is WalletState.MultiCurrency.Content -> { prevState.copy( walletCardState = prevState.walletCardState.toLoadedState(), tokensListState = prevState.tokensListState.toLoadedState(), + tangemPayMainUM = prevState.tangemPayMainUM.toLoadedState(), buttons = prevState.enableButtons(), ) } @@ -97,6 +105,17 @@ internal class SetTokenListTransformer( ).convert(value = this) } + private fun TangemPayMainUM.toLoadedState(): TangemPayMainUM { + val paymentAccountStatus = when (params) { + is TokenConverterParams.Account -> params.accountList.accountStatuses + .filterIsInstance() + .firstOrNull() + is TokenConverterParams.Wallet -> null + } ?: return this + + return tangemPayConverter.convert(paymentAccountStatus) + } + private fun toLoadedState(): WalletTokensListUM { if (params !is TokenConverterParams.Account) { return WalletTokensListUM.Empty( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt index e2c4d1a6c4..b8673b8774 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt @@ -18,7 +18,6 @@ internal class TangemPayRefreshNeededStateTransformer( override fun transform(prevState: WalletState): WalletState { val tangemPayState = TangemPayState.RefreshNeeded( notification = TangemPayRefreshNeeded( - tangemIcon = R.drawable.ic_tangem_24, buttonText = when (userWallet) { is UserWallet.Cold -> resourceReference(id = R.string.home_button_scan) is UserWallet.Hot -> resourceReference(id = R.string.tangempay_sync_needed_restore_access) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt index b37a6f916f..17ff78b37f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt @@ -5,9 +5,11 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM +import kotlinx.collections.immutable.toImmutableList internal class TangemPayRefreshShowProgressTransformer( userWalletId: UserWalletId, + private val shouldShowProgress: Boolean, ) : WalletStateTransformer(userWalletId) { override fun transform(prevState: WalletState): WalletState { @@ -15,11 +17,19 @@ internal class TangemPayRefreshShowProgressTransformer( val refreshNeededState = multiContentState.tangemPayState as? TangemPayState.RefreshNeeded ?: return prevState val refreshNotification = refreshNeededState.notification as? WalletNotification.Warning.TangemPayRefreshNeeded ?: return prevState + val newWarnings = prevState.warnings.map { warning -> + if (warning is WalletNotification.Warning.TangemPayRefreshNeeded) { + warning.copy(shouldShowProgress = shouldShowProgress) + } else { + warning + } + } return multiContentState.copy( tangemPayState = refreshNeededState.copy( - notification = refreshNotification.copy(shouldShowProgress = true), + notification = refreshNotification.copy(shouldShowProgress = shouldShowProgress), ), + warnings = newWarnings.toImmutableList(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt index 2c1c7e542f..6e44868ca7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt @@ -18,6 +18,7 @@ internal class UnlockWalletTransformer( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, + private val isTangemPayRefactorEnabled: Boolean, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { @@ -25,6 +26,7 @@ internal class UnlockWalletTransformer( clickIntents = clickIntents, walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt new file mode 100644 index 0000000000..41b74bc551 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt @@ -0,0 +1,110 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.pay.TangemPayDetailsConfig +import com.tangem.domain.visa.model.TangemPayCardFrozenState +import com.tangem.feature.wallet.child.wallet.model.intents.TangemPayIntents +import com.tangem.features.tangempay.entity.TangemPayMainUM +import com.tangem.utils.converter.Converter +import java.math.BigDecimal +import java.util.Currency + +private const val POLYGON_CHAIN_ID = 137 + +internal class TangemPayMainBlockConverter( + private val tangemPayClickIntents: TangemPayIntents, +) : Converter { + @Suppress("LongMethod", "CyclomaticComplexMethod") + override fun convert(value: AccountStatus.Payment): TangemPayMainUM { + return when (val statusValue = value.value) { + is PaymentAccountStatusValue.Error.CardIssueFailed -> TangemPayMainUM.FailedToIssue( + onClick = { tangemPayClickIntents.onIssuingFailedClicked(statusValue.customerId) }, + ) + is PaymentAccountStatusValue.Error.ExposedDevice -> TangemPayMainUM.ExposedDevice + is PaymentAccountStatusValue.Error.NotSynced -> TangemPayMainUM.SyncNeeded + is PaymentAccountStatusValue.Error.Unavailable -> TangemPayMainUM.TemporaryUnavailable + is PaymentAccountStatusValue.IssuingCard -> TangemPayMainUM.IssuingCard( + onClick = { tangemPayClickIntents.onIssuingCardClicked() }, + ) + is PaymentAccountStatusValue.UnderReview -> TangemPayMainUM.UnderReview( + subtitle = when (statusValue.kycStatus) { + KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed) + else -> TextReference.Res(R.string.tangempay_kyc_in_progress) + }, + onClick = { + when (statusValue.kycStatus) { + KycStatus.REJECTED -> tangemPayClickIntents.onKycRejectedClicked( + userWalletId = value.account.userWalletId, + customerId = statusValue.customerId, + ) + else -> tangemPayClickIntents.onKycProgressClicked(value.account.userWalletId) + } + }, + ) + is PaymentAccountStatusValue.NotCreated -> TangemPayMainUM.Empty + is PaymentAccountStatusValue.Loading -> TangemPayMainUM.Loading + is PaymentAccountStatusValue.Locked -> TangemPayMainUM.Content( + subtitle = stringReference("*${statusValue.lastFourDigits}"), + isBalanceFlickering = statusValue.source == StatusSource.CACHE, + balance = getBalanceText( + currencyCode = statusValue.currencyCode, + balance = statusValue.fiatBalance.availableBalance, + ), + balanceSubtitle = stringReference("USDC"), // TODO hardcode for now + shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE, + onClick = { + tangemPayClickIntents.openDetails( + value.account.userWalletId, + TangemPayDetailsConfig( + customerId = statusValue.customerId, + cardId = statusValue.cardId, + isPinSet = statusValue.isPinSet, + cardFrozenState = TangemPayCardFrozenState.Frozen, + cardNumberEnd = statusValue.lastFourDigits, + chainId = POLYGON_CHAIN_ID, + ), + ) + }, + ) + is PaymentAccountStatusValue.Loaded -> TangemPayMainUM.Content( + subtitle = stringReference("*${statusValue.lastFourDigits}"), + isBalanceFlickering = statusValue.source == StatusSource.CACHE, + balance = getBalanceText( + currencyCode = statusValue.currencyCode, + balance = statusValue.fiatBalance.availableBalance, + ), + balanceSubtitle = stringReference("USDC"), // TODO hardcode for now + shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE, + onClick = { + tangemPayClickIntents.openDetails( + value.account.userWalletId, + TangemPayDetailsConfig( + customerId = statusValue.customerId, + cardId = statusValue.cardId, + isPinSet = statusValue.isPinSet, + cardFrozenState = TangemPayCardFrozenState.Unfrozen, + cardNumberEnd = statusValue.lastFourDigits, + chainId = POLYGON_CHAIN_ID, + ), + ) + }, + ) + } + } + + private fun getBalanceText(currencyCode: String, balance: BigDecimal): TextReference { + val currency = Currency.getInstance(currencyCode) + val formattedBalance = balance.format { + fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol) + } + return stringReference(formattedBalance) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 06d9f69510..cb033c72d5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -17,6 +17,7 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.features.tangempay.entity.TangemPayMainUM import com.tangem.utils.extensions.addIf import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList @@ -33,6 +34,7 @@ internal class WalletLoadingStateFactory( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, + private val isTangemPayRefactorEnabled: Boolean, ) { fun create(userWallet: UserWallet): WalletState { @@ -82,6 +84,8 @@ internal class WalletLoadingStateFactory( nftState = WalletNFTItemUM.Hidden, type = WalletType.Hot, tangemPayState = TangemPayState.Empty, + tangemPayMainUM = TangemPayMainUM.Empty, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } @@ -96,6 +100,8 @@ internal class WalletLoadingStateFactory( nftState = WalletNFTItemUM.Hidden, type = WalletType.Cold, tangemPayState = TangemPayState.Empty, + tangemPayMainUM = TangemPayMainUM.Empty, + isTangemPayRefactorEnabled = isTangemPayRefactorEnabled, ) } 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 7e0a0747aa..8f842b82cf 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 @@ -49,7 +49,6 @@ import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetCo import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheet import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.expressTransactionsItems -import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetDraggableHeaderLegacy @@ -60,6 +59,7 @@ import com.tangem.core.ui.components.sheetscaffold.* import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar import com.tangem.core.ui.components.snackbar.TangemSnackbar import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.softLayerShadow import com.tangem.core.ui.extensions.stringResourceSafe @@ -84,6 +84,8 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.marketPriceBlock import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPayMainScreenBlock import com.tangem.feature.wallet.presentation.wallet.ui.utils.changeWalletAnimator +import com.tangem.features.tangempay.component.TangemPayMainBlockComponent +import com.tangem.features.tangempay.entity.TangemPayMainUM import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -92,6 +94,7 @@ import kotlin.math.roundToInt @Composable internal fun WalletScreen( state: WalletScreenState, + tangemPayComponent: TangemPayMainBlockComponent, promoBannersBlockComponent: ComposableContentComponent? = null, bottomSheetContent: @Composable (() -> Unit), bottomSheetHeaderHeightProvider: () -> Dp, @@ -106,6 +109,7 @@ internal fun WalletScreen( WalletContent( state = state, + tangemPayComponent = tangemPayComponent, walletsListState = walletsListState, snackbarHostState = snackbarHostState, isAutoScroll = isAutoScroll, @@ -128,6 +132,7 @@ internal fun WalletScreen( @Composable private fun WalletContent( state: WalletScreenState, + tangemPayComponent: TangemPayMainBlockComponent, walletsListState: LazyListState, snackbarHostState: SnackbarHostState, isAutoScroll: State, @@ -220,18 +225,12 @@ private fun WalletContent( } } - if (selectedWallet is WalletState.MultiCurrency) { - item( - key = "TangemPayMainScreenBlock", - contentType = selectedWallet.tangemPayState::class.java, - ) { - TangemPayMainScreenBlock( - state = selectedWallet.tangemPayState, - isBalanceHidden = state.isHidingMode, - modifier = itemModifier, - ) - } - } + tangemPayItem( + modifier = itemModifier, + state = selectedWallet, + isHidingMode = state.isHidingMode, + tangemPayComponent = tangemPayComponent, + ) (selectedWallet as? WalletState.SingleCurrency)?.let { walletState -> walletState.marketPriceBlockState?.let { marketPriceBlockState -> @@ -749,6 +748,25 @@ internal fun LazyListScope.nftCollections(state: WalletState, itemModifier: Modi } } +internal fun LazyListScope.tangemPayItem( + state: WalletState, + isHidingMode: Boolean, + tangemPayComponent: TangemPayMainBlockComponent, + modifier: Modifier = Modifier, +) { + if (state !is WalletState.MultiCurrency) return + + if (state.isTangemPayRefactorEnabled) { + with(tangemPayComponent) { + tangemPayMainContent(modifier = modifier, state = state.tangemPayMainUM, isBalanceHidden = isHidingMode) + } + } else { + item(key = "TangemPayMainScreenBlock", contentType = state.tangemPayState::class.java) { + TangemPayMainScreenBlock(modifier = modifier, state = state.tangemPayState, isBalanceHidden = isHidingMode) + } + } +} + @Composable private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { if (bottomSheetConfig != null) { @@ -767,6 +785,14 @@ private fun WalletScreen_Preview(@PreviewParameter(WalletScreenPreviewProvider:: TangemThemePreview { WalletScreen( state = data, + tangemPayComponent = object : TangemPayMainBlockComponent { + override fun LazyListScope.tangemPayMainContent( + state: TangemPayMainUM, + isBalanceHidden: Boolean, + modifier: Modifier, + ) { + } + }, bottomSheetContent = { Text("Markets Content") }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 90fc34c41f..8d058b28bc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -3,10 +3,13 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier +import com.tangem.common.ui.notifications.CreatePaymentAccountNotification import com.tangem.core.ui.components.notifications.NoteMigrationNotification import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.ForceDarkTheme import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import kotlinx.collections.immutable.ImmutableList @@ -49,6 +52,16 @@ internal fun LazyListScope.notifications(configs: ImmutableList { + CreatePaymentAccountNotification( + modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), + onClick = item.onClick, + onCloseClick = item.onCloseClick, + image = R.drawable.img_tangem_pay_visa_banner, + title = resourceReference(R.string.tangempay_onboarding_banner_title), + subtitle = resourceReference(R.string.tangempay_onboarding_banner_description), + ) + } else -> { Notification( config = item.config, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt index fbf1117370..862b6c770d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt @@ -13,6 +13,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification.Warning.TangemPayRefreshNeeded import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TangemPayCardMainBlock @@ -41,7 +42,6 @@ private fun TangemPayMainScreenBlockPreview() { TangemPayMainScreenBlock( state = TangemPayState.RefreshNeeded( TangemPayRefreshNeeded( - tangemIcon = R.drawable.ic_tangem_24, buttonText = resourceReference(id = R.string.home_button_scan), onRefreshClick = {}, shouldShowProgress = false, @@ -49,13 +49,30 @@ private fun TangemPayMainScreenBlockPreview() { ), isBalanceHidden = false, ) + TangemPayMainScreenBlock( + state = TangemPayState.TemporaryUnavailable(WalletNotification.Warning.TangemPayUnreachable), + isBalanceHidden = false, + ) + TangemPayMainScreenBlock( + state = TangemPayState.OnboardingBanner(onClick = {}, closeOnClick = {}), + isBalanceHidden = false, + ) TangemPayMainScreenBlock(state = TangemPayState.ExposedDevice, isBalanceHidden = false) + TangemPayMainScreenBlock( + state = TangemPayState.FailedIssue( + title = TextReference.Res(R.string.tangempay_payment_account), + description = TextReference.Res(R.string.tangempay_failed_to_issue_card), + iconRes = R.drawable.ic_alert_24, + onButtonClick = { }, + ), + isBalanceHidden = false, + ) TangemPayMainScreenBlock( Progress( - title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title), - description = TextReference.EMPTY, + title = TextReference.Res(R.string.tangempay_payment_account), + description = TextReference.Res(R.string.tangempay_kyc_in_progress), buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), iconRes = R.drawable.ic_promo_kyc_36, onButtonClick = {}, @@ -65,19 +82,8 @@ private fun TangemPayMainScreenBlockPreview() { TangemPayMainScreenBlock( Progress( - title = TextReference.Res(R.string.tangempay_issue_card_notification_title), - description = TextReference.EMPTY, - buttonText = TextReference.Res(R.string.common_continue), - iconRes = R.drawable.ic_tangem_pay_promo_card_36, - onButtonClick = {}, - ), - isBalanceHidden = false, - ) - - TangemPayMainScreenBlock( - Progress( - title = TextReference.Res(R.string.tangempay_issue_card_notification_title), - description = TextReference.Res(R.string.tangempay_issue_card_notification_description), + title = TextReference.Res(R.string.tangempay_payment_account), + description = TextReference.Res(R.string.tangempay_issuing_your_card), buttonText = TextReference.EMPTY, iconRes = R.drawable.ic_tangem_pay_promo_card_36, onButtonClick = {}, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt index b3c06f3a28..715a6168a2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayRefreshBlock.kt @@ -67,7 +67,6 @@ private fun TangemPayRefreshBlockPreview() { TangemPayRefreshBlock( state = TangemPayState.RefreshNeeded( TangemPayRefreshNeeded( - tangemIcon = R.drawable.ic_tangem_24, buttonText = resourceReference(id = R.string.tangempay_sync_needed_restore_access), onRefreshClick = {}, shouldShowProgress = true, From d376a7118daac46bf677a369ecbbeeb43eacc512 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Mar 2026 11:11:09 +0400 Subject: [PATCH 11/75] Updated on 2026-08-14 --- .../com/tangem/tap/common/DialogManager.kt | 27 --------- .../com/tangem/tap/common/redux/AppDialog.kt | 32 ----------- .../tangem/tap/common/ui/SimpleAlertDialog.kt | 55 ------------------- .../tangem/tap/common/ui/SimpleOkDialog.kt | 31 ----------- .../cardsettings/model/CardSettingsModel.kt | 13 ++--- .../ui/dialogs/WalletAlreadyWasUsedDialog.kt | 33 ----------- .../ui/userwallet/UserWalletUnlockError.kt | 8 +-- core/res/src/main/res/values-de/strings.xml | 1 + core/res/src/main/res/values-es/strings.xml | 1 + core/res/src/main/res/values-fr/strings.xml | 1 + core/res/src/main/res/values-it/strings.xml | 1 + core/res/src/main/res/values-ja/strings.xml | 4 +- .../src/main/res/values-pt-rBR/strings.xml | 1 + core/res/src/main/res/values-ru/strings.xml | 1 + .../src/main/res/values-uk-rUA/strings.xml | 1 + .../src/main/res/values-zh-rTW/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 7 ++- .../tangem/core/ui/message/dialog/Dialogs.kt | 16 ++++++ .../com/tangem/domain/redux/StateDialog.kt | 2 - .../CreateWalletStartModel.kt | 9 +-- .../features/home/impl/model/HomeModel.kt | 11 +--- .../CreateHardwareWalletModel.kt | 9 +-- 22 files changed, 44 insertions(+), 221 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/ui/SimpleOkDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletAlreadyWasUsedDialog.kt diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index c3a367a894..7c984b6d60 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -3,16 +3,11 @@ package com.tangem.tap.common import android.app.Dialog import android.content.Context import com.tangem.domain.redux.StateDialog -import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.ui.ScanFailsDialog -import com.tangem.tap.common.ui.SimpleAlertDialog -import com.tangem.tap.common.ui.SimpleOkDialog import com.tangem.tap.features.onboarding.OnboardingDialog import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletActivationErrorDialog -import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletAlreadyWasUsedDialog import com.tangem.tap.store -import com.tangem.wallet.R import org.rekotlin.StoreSubscriber class DialogManager : StoreSubscriber { @@ -44,34 +39,12 @@ class DialogManager : StoreSubscriber { if (dialog != null) return dialog = when (state.dialog) { - is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context) is StateDialog.ScanFailsDialog -> ScanFailsDialog.create( context = context, source = state.dialog.source, onTryAgain = state.dialog.onTryAgain, ) - is StateDialog.NfcFeatureIsUnavailable -> SimpleAlertDialog.create( - titleRes = R.string.common_error, - messageRes = R.string.nfc_error_unavailable, - context = context, - ) is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog) - is AppDialog.TokensAreLinkedDialog -> SimpleAlertDialog.create( - title = context.getString(state.dialog.titleRes, state.dialog.currencySymbol), - message = context.getString( - state.dialog.messageRes, - state.dialog.currencyTitle, - state.dialog.currencySymbol, - state.dialog.networkName, - ), - context = context, - ) - is AppDialog.WalletAlreadyWasUsedDialog -> WalletAlreadyWasUsedDialog.create( - context = context, - onOk = state.dialog.onOk, - onSupport = state.dialog.onSupportClick, - onCancel = state.dialog.onCancel, - ) else -> null } dialog?.show() diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt b/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt deleted file mode 100644 index 69ef67c88a..0000000000 --- a/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.tap.common.redux - -import com.tangem.common.extensions.VoidCallback -import com.tangem.domain.redux.StateDialog -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -sealed class AppDialog : StateDialog { - data class SimpleOkDialogRes( - val headerId: Int, - val messageId: Int, - val args: List = emptyList(), - val onOk: VoidCallback? = null, - ) : AppDialog() - - data class TokensAreLinkedDialog( - val currencyTitle: String, - val currencySymbol: String, - val networkName: String, - ) : AppDialog() { - val messageRes: Int = R.string.token_details_unable_hide_alert_message - val titleRes: Int = R.string.token_details_unable_hide_alert_title - } - - data class WalletAlreadyWasUsedDialog( - val onOk: () -> Unit, - val onSupportClick: () -> Unit, - val onCancel: () -> Unit, - ) : AppDialog() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt deleted file mode 100644 index 078691ca7c..0000000000 --- a/app/src/main/java/com/tangem/tap/common/ui/SimpleAlertDialog.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.tangem.tap.common.ui - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.store -import com.tangem.wallet.R - -object SimpleAlertDialog { - fun create( - titleRes: Int? = null, - messageRes: Int? = null, - title: String? = null, - message: String? = null, - primaryButtonRes: Int = R.string.common_ok, - context: Context, - ): AlertDialog { - return SimpleCancelableAlertDialog.create( - titleRes = titleRes, - messageRes = messageRes, - title = title, - message = message, - primaryButtonRes = primaryButtonRes, - secondaryButtonRes = null, - context = context, - ) - } -} - -object SimpleCancelableAlertDialog { - fun create( - titleRes: Int? = null, - messageRes: Int? = null, - title: String? = null, - message: String? = null, - primaryButtonRes: Int = R.string.common_ok, - secondaryButtonRes: Int? = R.string.common_cancel, - primaryButtonAction: () -> Unit = {}, - secondaryButtonAction: () -> Unit = {}, - context: Context, - ): AlertDialog { - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(if (titleRes != null) context.getString(titleRes) else title) - setMessage(if (messageRes != null) context.getString(messageRes) else message) - setPositiveButton(context.getText(primaryButtonRes)) { _, _ -> primaryButtonAction() } - if (secondaryButtonRes != null) { - setNegativeButton(context.getText(secondaryButtonRes)) { _, _ -> secondaryButtonAction() } - } - setOnDismissListener { - store.dispatch(GlobalAction.HideDialog) - } - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/ui/SimpleOkDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/SimpleOkDialog.kt deleted file mode 100644 index e0eb7acf6f..0000000000 --- a/app/src/main/java/com/tangem/tap/common/ui/SimpleOkDialog.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.tap.common.ui - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.tangem.tap.common.extensions.dispatchDialogHide -import com.tangem.tap.common.redux.AppDialog -import com.tangem.tap.store -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -object SimpleOkDialog { - - fun create(dialog: AppDialog.SimpleOkDialogRes, context: Context): AlertDialog { - val message = if (dialog.args.isEmpty()) { - context.getString(dialog.messageId) - } else { - context.getString(dialog.messageId, *dialog.args.toTypedArray()) - } - return AlertDialog.Builder(context).apply { - setTitle(context.getString(dialog.headerId)) - setMessage(message) - setPositiveButton(R.string.common_ok) { _, _ -> } - setOnDismissListener { - store.dispatchDialogHide() - dialog.onOk?.invoke() - } - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt index b395cc0bf5..ecce5aaf75 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt @@ -10,6 +10,8 @@ import com.tangem.core.analytics.Analytics import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.message.dialog.Dialogs import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.common.util.cardTypesResolver @@ -25,9 +27,7 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase 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.features.details.ui.cardsettings.CardInfo import com.tangem.tap.features.details.ui.cardsettings.CardSettingsScreenState import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent @@ -37,7 +37,6 @@ import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addIf import com.tangem.utils.logging.TangemLogger -import com.tangem.wallet.R import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -56,6 +55,7 @@ internal class CardSettingsModel @Inject constructor( private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsRepository: SettingsRepository, private val onboardingRepository: OnboardingRepository, + private val uiMessageSender: UiMessageSender, ) : Model() { private val params = paramsContainer.require() @@ -115,12 +115,7 @@ internal class CardSettingsModel @Inject constructor( if (userWalletId == scannedUserWalletId || scannedUserWalletId == null) { cardSettingsInteractor.initialize(scanResponse) } else { - store.dispatchDialogShow( - AppDialog.SimpleOkDialogRes( - headerId = R.string.common_warning, - messageId = R.string.error_wrong_wallet_tapped, - ), - ) + uiMessageSender.send(Dialogs.wrongWalletTapped()) } } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletAlreadyWasUsedDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletAlreadyWasUsedDialog.kt deleted file mode 100644 index 6d5875be78..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletAlreadyWasUsedDialog.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.store -import com.tangem.wallet.R - -object WalletAlreadyWasUsedDialog { - - fun create(context: Context, onOk: () -> Unit, onCancel: () -> Unit, onSupport: () -> Unit): AlertDialog { - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(R.string.security_alert_title) - setMessage(R.string.wallet_been_activated_message) - setPositiveButton(R.string.this_is_my_wallet_title) { dialog, _ -> - onOk() - dialog.dismiss() - } - setNeutralButton(R.string.common_cancel) { dialog, _ -> - onCancel() - dialog.dismiss() - } - setNegativeButton(R.string.alert_button_request_support) { dialog, _ -> - onSupport() - dialog.dismiss() - } - setOnDismissListener { - store.dispatch(GlobalAction.HideDialog) - } - }.create() - } -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt index 41fdd9e856..dd99983320 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletUnlockError.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessage +import com.tangem.core.ui.message.dialog.Dialogs import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.common.wallets.error.UnlockWalletError import com.tangem.domain.common.wallets.error.UnlockWalletError.UnableToUnlock.Reason @@ -22,12 +23,7 @@ inline fun UnlockWalletError.handle( when (this) { UnlockWalletError.AlreadyUnlocked -> onAlreadyUnlocked() UnlockWalletError.ScannedCardWalletNotMatched -> { - showMessage( - DialogMessage( - title = resourceReference(R.string.common_warning), - message = resourceReference(R.string.error_wrong_wallet_tapped), - ), - ) + showMessage(Dialogs.wrongWalletTapped()) } UnlockWalletError.UserCancelled -> onUserCancelled() UnlockWalletError.UserWalletNotFound -> { diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index c4760bb8a5..72e7efe111 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -404,6 +404,7 @@ Aufgrund der Beschränkungen von %1$s können nur %2$d UTXOs in eine einzige Transaktion passen. Das bedeutet, dass du nur %3$s oder weniger senden kannst. Du musst den Betrag reduzieren. Wert kopiert Meine Wallet + Warnung Woche mit Ja diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 4fde82b922..1b40ed9ebf 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -401,6 +401,7 @@ Debido a limitaciones sobre %1$s, solo %2$d UTXO pueden caber en una sola transacción. Esto significa que solo puedes enviar %3$s o menos. Debe reducir la cantidad. Valor copiado Billeteras + Alerta semana con diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 89c9435c9b..772f4d92d2 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -400,6 +400,7 @@ Unstakez En raison d\'une limitations sur les %1$s, seuls les %2$d UTXO peuvent s\'intégrer dans une seule transaction. Ce qui signifie vous ne pouvez envoyer que %3$s ou moins. Réduisez le montant. Valeur copiée + Alerte semaine avec Oui diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 9cd39c1c7a..0282ef7661 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -26,6 +26,7 @@ %d gettone %d gettoni + Avviso Codice di accesso Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto Mantenimento della carta diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 6efdf8cd26..e4a52817fd 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -301,6 +301,7 @@ 時間 インポート 進行中 + 残高不足 後で もっと詳しく 残り%1$s @@ -397,6 +398,7 @@ %1$sの制限により、1つのトランザクションに収まるUTXOは%2$d個のみです。つまり、 %3$s以下しか送信できません。量を減らす必要があります。 値がコピーされました ウォレット + 警告 はい @@ -473,7 +475,7 @@ 絞り込み マイネットワーク ネットワーク - よく使われています + 人気 該当する結果はありません ステーキング・利息モード こんにちは、サポートチームの皆さん、コード %s のエラーが発生しました。 diff --git a/core/res/src/main/res/values-pt-rBR/strings.xml b/core/res/src/main/res/values-pt-rBR/strings.xml index 04ca28b1d4..de83ee0e8f 100644 --- a/core/res/src/main/res/values-pt-rBR/strings.xml +++ b/core/res/src/main/res/values-pt-rBR/strings.xml @@ -404,6 +404,7 @@ Devido a %1$s limitações apenas %2$d Os UTXOs podem caber em uma única transação. Isso significa que você só pode enviar %3$s ou menos. Você precisa reduzir a quantidade. Valor copiado Carteiras + Aviso semana com Sim diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index a624ed8ccd..2654c8ae24 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -415,6 +415,7 @@ Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. Значение скопировано Кошельки + Предупреждение неделю с Да 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 14557017ad..67cdf8af99 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -415,6 +415,7 @@ Через обмеження %1$s в одну транзакцію може поміститися тільки %2$d UTXO. Це означає, що ви можете відправити тільки %3$s або менше. Вам потрібно зменшити суму. Скопійовано Гаманці + Увага тиждень з Так diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index c410e833bc..c18d1cec74 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -99,6 +99,7 @@ 我了解 無法觸達 由於 %1$s 的限制,只有%2$d UTXO 可以放入單次交易中。這意味著您只能發送%3$s或更少數量。您需要減少數量。 + 警告 已複製代幣地址 支持的網路 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 5f645487de..02c1920d57 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -405,6 +405,7 @@ Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. Value copied Wallets + Warning week with Yes @@ -481,7 +482,7 @@ Filter by My networks Networks - Mostly used + Popular No results Staking & Yield mode Hi support team, I\'ve encountered an error with code: %s @@ -703,7 +704,7 @@ Not enough Mana You can transfer only %s due to the Mana limit imposed by the Koinos network Mana limit - The Koinos network requires Mana for network fees. Your have %1$s/%2$s Mana + The Koinos network requires Mana for network fees. You have %1$s/%2$s Mana Mana level To begin tracking your crypto assets and transactions, add tokens Manage tokens @@ -829,6 +830,7 @@ Position in crypto rating between all coins based on market capitalization Market position Max supply + Circulating and Max Supply The maximum number of coins or tokens that can ever exist for a particular cryptocurrency Max supply Metrics @@ -1666,6 +1668,7 @@ Token in %%image%% %1$s network The %1$s (%2$s) token is the main currency on the %3$s network and cannot be hidden as long as you have other tokens on this network in the list Unable to hide %s + N/A Show QR code Exchange this token for another at %1$s service fees from February %2$s-%3$s. Swap with Changelly, %s fees diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt index dec4c24085..ff431ff036 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt @@ -13,6 +13,22 @@ import com.tangem.core.ui.message.EventMessageAction */ object Dialogs { + /** + * NFC feature is unavailable dialog + */ + fun nfcFeatureUnavailable(): DialogMessage = DialogMessage( + title = resourceReference(id = R.string.common_error), + message = resourceReference(R.string.nfc_error_unavailable), + ) + + /** + * Wrong wallet tapped dialog + */ + fun wrongWalletTapped(): DialogMessage = DialogMessage( + title = resourceReference(id = R.string.common_warning), + message = resourceReference(id = R.string.error_wrong_wallet_tapped), + ) + /** * Card verification failed dialog * diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt index 9d5084361d..5299850377 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt @@ -2,8 +2,6 @@ package com.tangem.domain.redux interface StateDialog { - data object NfcFeatureIsUnavailable : StateDialog - data class ScanFailsDialog(val source: ScanFailsSource, val onTryAgain: (() -> Unit)? = null) : StateDialog enum class ScanFailsSource { diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt index 4a864f1ca9..5f590c887c 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt @@ -18,7 +18,7 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.dialog.Dialogs import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.card.ScanCardProcessor @@ -252,11 +252,6 @@ internal class CreateWalletStartModel @Inject constructor( } private fun handleNfcFeatureUnavailable() { - uiMessageSender.send( - message = DialogMessage( - message = resourceReference(R.string.nfc_error_unavailable), - title = resourceReference(id = R.string.common_error), - ), - ) + uiMessageSender.send(Dialogs.nfcFeatureUnavailable()) } } \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt index 63a48788e4..97c6d2a93c 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -17,9 +17,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.R -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.dialog.Dialogs import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.analytics.IntroductionProcess import com.tangem.domain.card.analytics.ParamCardCurrencyConverter @@ -251,11 +249,6 @@ internal class HomeModel @Inject constructor( } private fun handleNfcFeatureUnavailable() { - uiMessageSender.send( - message = DialogMessage( - message = resourceReference(R.string.nfc_error_unavailable), - title = resourceReference(id = R.string.common_error), - ), - ) + uiMessageSender.send(Dialogs.nfcFeatureUnavailable()) } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt index d3e5315e41..c11626ce87 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt @@ -12,9 +12,9 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.dialog.Dialogs import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.analytics.IntroductionProcess import com.tangem.domain.card.repository.CardSdkConfigRepository @@ -170,12 +170,7 @@ internal class CreateHardwareWalletModel @Inject constructor( } private fun handleNfcFeatureUnavailable() { - uiMessageSender.send( - message = DialogMessage( - message = resourceReference(R.string.nfc_error_unavailable), - title = resourceReference(id = R.string.common_error), - ), - ) + uiMessageSender.send(Dialogs.nfcFeatureUnavailable()) } private suspend fun handleAlreadySavedCard(messageId: Int, walletId: UserWalletId, scanResponse: ScanResponse) { From c7e4ff059853027afc18236ecb46b7367b3823fe Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Mar 2026 16:36:05 +0700 Subject: [PATCH 12/75] Updated on 2026-08-14 --- .../field/AmountFieldChangeTransformer.kt | 2 +- .../api/express/TangemExpressApi.kt | 1 + .../models/response/ExchangeQuoteResponse.kt | 3 + core/ui/src/main/res/drawable/ic_fixed_32.xml | 9 +++ .../src/main/res/drawable/ic_floating_32.xml | 9 +++ .../data/swap/DefaultSwapRepositoryV2.kt | 3 + .../domain/swap/models/SwapQuoteModel.kt | 1 + .../tangem/domain/swap/SwapRepositoryV2.kt | 1 + .../domain/swap/usecase/GetSwapDataUseCase.kt | 2 + .../v2/impl/amount/SwapAmountComponent.kt | 30 ++++++++ .../v2/impl/amount/model/SwapAmountModel.kt | 4 +- .../model/converter/SwapQuoteUMConverter.kt | 2 + .../SwapAmountErrorQuoteTransformer.kt | 22 +++++- .../SwapAmountSelectQuoteTransformer.kt | 2 +- .../SwapAmountSetQuotesTransformer.kt | 68 ++++++++++--------- .../swap/v2/impl/common/ConfirmData.kt | 3 +- .../swap/v2/impl/common/SwapAlertFactory.kt | 2 +- .../swap/v2/impl/common/entity/SwapQuoteUM.kt | 1 + .../SwapNotificationsComponent.kt | 3 + .../entity/SwapNotificationUM.kt | 5 ++ .../model/SwapNotificationsModel.kt | 9 +++ .../confirm/SendWithSwapConfirmComponent.kt | 4 +- .../confirm/model/SendWithSwapConfirmModel.kt | 22 ++++-- .../confirm/model/SwapTransactionSender.kt | 33 ++++++--- .../rateinfo/SendWithSwapRateInfoFactory.kt | 33 +++++++++ .../rateinfo/SwapRateInfoComponent.kt | 27 ++++++++ .../feature/swap/DefaultSwapRepository.kt | 1 + 27 files changed, 245 insertions(+), 57 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_fixed_32.xml create mode 100644 core/ui/src/main/res/drawable/ic_floating_32.xml create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SendWithSwapRateInfoFactory.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SwapRateInfoComponent.kt 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 4187ce21d2..c4a063c28b 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 @@ -73,7 +73,7 @@ class AmountFieldChangeTransformer( fiatValue = fiatValue, isError = isCheckFailed, error = when { - isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance) + isExceedBalance -> resourceReference(R.string.common_insufficient_balance) isLessThanMinimumIfProvided -> { val minimumAmount = minimumTransactionAmount.amount.format { crypto(cryptoCurrencyStatus.currency) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index b64ceb1ec2..4d63fd4b3a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -70,6 +70,7 @@ interface TangemExpressApi { @Query("refundExtraId") refundExtraId: String?, // for cex only @Query("partnerOperationType") partnerOperationType: String?, // swap/ swap-and-send @Query("toExtraId") toExtraId: String?, // swap-and-send memo + @Query("quoteId") quoteId: String?, // swap-and-send memo ): ApiResponse @GET("exchange-status") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt index d08ebf45bc..4326d871bf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeQuoteResponse.kt @@ -25,4 +25,7 @@ data class ExchangeQuoteResponse( @Json(name = "minAmount") val minAmount: BigDecimal, + @Json(name = "quoteId") + val quoteId: String? = null, + ) \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_fixed_32.xml b/core/ui/src/main/res/drawable/ic_fixed_32.xml new file mode 100644 index 0000000000..3197cc1649 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_fixed_32.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_floating_32.xml b/core/ui/src/main/res/drawable/ic_floating_32.xml new file mode 100644 index 0000000000..c2ccfb5a40 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_floating_32.xml @@ -0,0 +1,9 @@ + + + diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index f5d7042b43..d51b43b1e1 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -221,6 +221,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( toTokenAmount = toTokenAmount, fromTokenAmount = fromTokenAmount, allowanceContract = response.allowanceContract, + quoteId = response.quoteId, ) } @@ -235,6 +236,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( expressProvider: ExpressProvider, rateType: ExpressRateType, expressOperationType: ExpressOperationType, + quoteId: String?, ): SwapDataModel = withContext(coroutineDispatcher.io) { val requestId = UUID.randomUUID().toString() val (fromCurrency, fromStatus) = fromCryptoCurrencyStatus @@ -273,6 +275,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( appPreferencesStore = appPreferencesStore, ), toExtraId = toExtraId?.ifEmpty { null }, + quoteId = quoteId, ).getOrThrow() if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) { diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt index 45df45fc76..bd321cbf88 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapQuoteModel.kt @@ -16,4 +16,5 @@ data class SwapQuoteModel( val toTokenAmount: BigDecimal, val fromTokenAmount: BigDecimal?, val allowanceContract: String?, + val quoteId: String? = null, ) \ No newline at end of file diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt index b456087ac4..dacec2c058 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt @@ -91,6 +91,7 @@ interface SwapRepositoryV2 { expressProvider: ExpressProvider, rateType: ExpressRateType, expressOperationType: ExpressOperationType, + quoteId: String?, ): SwapDataModel /** diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt index 448e5ddcd5..2726e77e94 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt @@ -30,6 +30,7 @@ class GetSwapDataUseCase( expressProvider: ExpressProvider, rateType: ExpressRateType, expressOperationType: ExpressOperationType, + quoteId: String? = null, ): Either = Either.catch { swapRepositoryV2.getSwapData( userWallet = userWallet, @@ -42,6 +43,7 @@ class GetSwapDataUseCase( expressProvider = expressProvider, rateType = rateType, expressOperationType = expressOperationType, + quoteId = quoteId, ) }.mapLeft { throwable -> swapErrorResolver.resolve(throwable) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt index 035c016356..91b726bd17 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponent.kt @@ -5,15 +5,23 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.essenty.lifecycle.subscribe import com.tangem.common.ui.navigationButtons.NavigationModelCallback 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.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.express.models.ExpressRateType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountModel import com.tangem.features.swap.v2.impl.amount.ui.SwapAmountContent +import com.tangem.features.swap.v2.impl.sendviaswap.rateinfo.SwapRateInfoComponent import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -24,6 +32,14 @@ internal class SwapAmountComponent @AssistedInject constructor( private val model: SwapAmountModel = getOrCreateModel(params = params) + private val rateInfoSlot = childSlot( + source = model.rateInfoNavigation, + key = "rateInfoSlot", + serializer = null, + handleBackButton = true, + childFactory = ::rateInfoChild, + ) + init { lifecycle.subscribe( onStart = model::onStart, @@ -36,12 +52,26 @@ internal class SwapAmountComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { val amountUM by model.uiState.collectAsStateWithLifecycle() + val rateInfo by rateInfoSlot.subscribeAsState() SwapAmountContent( amountUM = amountUM, modifier = Modifier.background(TangemTheme.colors.background.tertiary), clickIntents = model, ) + + rateInfo.child?.instance?.BottomSheet() + } + + private fun rateInfoChild( + config: ExpressRateType, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent { + return SwapRateInfoComponent( + appComponentContext = childByContext(componentContext), + expressRateType = config, + onDismiss = { model.rateInfoNavigation.dismiss() }, + ) } interface ModelCallback : NavigationModelCallback { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index ac2fe177b4..98c238edb1 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -109,6 +109,7 @@ internal class SwapAmountModel @Inject constructor( private var userCountry: UserCountry = UserCountry.Other(Locale.getDefault().country) val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val rateInfoNavigation: SlotNavigation = SlotNavigation() private var isShowBestRateAnimation: Boolean = false @@ -318,7 +319,8 @@ internal class SwapAmountModel @Inject constructor( } override fun onRateClick() { - // TODO [REDACTED_TASK_KEY] + val content = uiState.value as? SwapAmountUM.Content ?: return + rateInfoNavigation.activate(content.swapRateType) } override fun onSeparatorClick() { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt index 3f5255f097..c521bbceb9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapQuoteUMConverter.kt @@ -64,6 +64,7 @@ internal class SwapQuoteUMConverter( fromAmountValue = fromAmountValue, rate = annotatedReference(rateString), isSingleProvider = false, + quoteId = quote.quoteId, ) } } else { @@ -78,6 +79,7 @@ internal class SwapQuoteUMConverter( fromAmountValue = fromAmountValue, rate = annotatedReference(rateString), isSingleProvider = false, + quoteId = quote.quoteId, ) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountErrorQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountErrorQuoteTransformer.kt index edb2d9e257..122e946132 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountErrorQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountErrorQuoteTransformer.kt @@ -5,17 +5,23 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountErrorConverter +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.domain.swap.models.SwapAmountType import com.tangem.utils.transformer.Transformer /** * Sets a generic "Something went wrong" error on the selected amount field - * when all quotes returned errors. + * when all quotes returned errors that cannot be converted to a specific amount error. + * Returns [prevState] unchanged when the condition is not met. */ -internal object SwapAmountErrorQuoteTransformer : Transformer { +internal class SwapAmountErrorQuoteTransformer( + private val quotes: List, +) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState + if (!areAllQuotesUnrecoverableErrors(prevState)) return prevState val error = resourceReference(R.string.send_with_swap_something_went_wrong) @@ -40,6 +46,18 @@ internal object SwapAmountErrorQuoteTransformer : Transformer { ) } + private fun areAllQuotesUnrecoverableErrors(state: SwapAmountUM.Content): Boolean { + if (!quotes.all { it is SwapQuoteUM.Error }) return false + + val secondaryProviderErrorConverter = state.secondaryCryptoCurrencyStatus?.let { + SwapAmountErrorConverter(cryptoCurrency = it.currency) + } + return quotes.all { quote -> + (quote as? SwapQuoteUM.Error)?.expressError + ?.let { secondaryProviderErrorConverter?.convert(it) } == null + } + } + private fun applyError( field: SwapAmountFieldUM, error: com.tangem.core.ui.extensions.TextReference, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt index 8e1878c21c..627c8e5881 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt @@ -140,7 +140,7 @@ internal class SwapAmountSelectQuoteTransformer( val insufficientFundsError = if (isSecondarySelected && fromAmount != null) { val primaryBalance = prevState.primaryCryptoCurrencyStatus.value.amount if (primaryBalance != null && fromAmount > primaryBalance) { - resourceReference(R.string.swapping_insufficient_funds) + resourceReference(R.string.common_insufficient_balance) } else { null } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index 9349099eb2..9b84a536bf 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -28,36 +28,30 @@ internal class SwapAmountSetQuotesTransformer( private val primaryMaximumAmountBoundary: EnterAmountBoundary? = null, private val primaryMinimumAmountBoundary: EnterAmountBoundary? = null, ) : Transformer { + override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState val selectedAmountType = prevState.selectedAmountType val comparator = SwapQuotesComparator(selectedAmountType) - - val isSingleProvider = quotes.filter { swapQuoteUM -> - swapQuoteUM is SwapQuoteUM.Content || swapQuoteUM is SwapQuoteUM.Allowance || - (swapQuoteUM as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError - }.isSingleItem() - + val isSingleProvider = isSingleProvider(quotes) val sortedQuotes = quotes.sortedWith(comparator) val bestQuote = findBestQuote(quotes, comparator) ?: SwapQuoteUM.Empty + val quotesWithDiff = getQuotesWithDiff( sortedQuotes = sortedQuotes, bestQuote = bestQuote, isSingleProvider = isSingleProvider, selectedAmountType = selectedAmountType, ) - val selectedQuote = if (isSilentReload && prevState.selectedQuote !is SwapQuoteUM.Loading) { - quotesWithDiff.firstOrNull { it.provider?.providerId == prevState.selectedQuote.provider?.providerId } - ?: prevState.selectedQuote - } else { - (bestQuote as? SwapQuoteUM.Content)?.copy( - diffPercent = DifferencePercent.Best, - isSingleProvider = isSingleProvider, - ) ?: bestQuote - } + val selectedQuote = resolveSelectedQuote( + prevState = prevState, + quotesWithDiff = quotesWithDiff, + bestQuote = bestQuote, + isSingleProvider = isSingleProvider, + ) - val selectQuoteTransformer = SwapAmountSelectQuoteTransformer( + val updatedState = SwapAmountSelectQuoteTransformer( quoteUM = selectedQuote, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, @@ -66,30 +60,42 @@ internal class SwapAmountSetQuotesTransformer( isBalanceHidden = isBalanceHidden, primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, - ) - - val updatedState = selectQuoteTransformer.transform(prevState = prevState) + ).transform(prevState = prevState) if (updatedState !is SwapAmountUM.Content) return prevState - val areAllQuotesErrors = quotes.all { it is SwapQuoteUM.Error } - val stateWithError = if (areAllQuotesErrors) { - SwapAmountErrorQuoteTransformer.transform(updatedState) - } else { - updatedState - } + val stateWithError = SwapAmountErrorQuoteTransformer(quotes).transform(updatedState) if (stateWithError !is SwapAmountUM.Content) return prevState return stateWithError.copy( isPrimaryButtonEnabled = stateWithError.isPrimaryButtonEnabled && quotesWithDiff.isNotEmpty(), - swapQuotes = getQuotesWithDiff( - sortedQuotes = sortedQuotes, - bestQuote = bestQuote, - isSingleProvider = isSingleProvider, - selectedAmountType = selectedAmountType, - ), + swapQuotes = quotesWithDiff, ) } + private fun resolveSelectedQuote( + prevState: SwapAmountUM.Content, + quotesWithDiff: ImmutableList, + bestQuote: SwapQuoteUM, + isSingleProvider: Boolean, + ): SwapQuoteUM { + if (isSilentReload && prevState.selectedQuote !is SwapQuoteUM.Loading) { + return quotesWithDiff + .firstOrNull { it.provider?.providerId == prevState.selectedQuote.provider?.providerId } + ?: prevState.selectedQuote + } + return (bestQuote as? SwapQuoteUM.Content)?.copy( + diffPercent = DifferencePercent.Best, + isSingleProvider = isSingleProvider, + ) ?: bestQuote + } + + private fun isSingleProvider(quotes: List): Boolean { + return quotes.filter { swapQuoteUM -> + swapQuoteUM is SwapQuoteUM.Content || swapQuoteUM is SwapQuoteUM.Allowance || + (swapQuoteUM as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError + }.isSingleItem() + } + private fun getQuotesWithDiff( sortedQuotes: List, bestQuote: SwapQuoteUM, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt index 7da47a57aa..f42349e6c2 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt @@ -10,7 +10,8 @@ import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import java.math.BigDecimal internal data class ConfirmData( - val enteredAmount: BigDecimal?, + val enteredFromAmount: BigDecimal?, + val enteredToAmount: BigDecimal?, val reduceAmountBy: BigDecimal, val isIgnoreReduce: Boolean, val enteredDestination: String?, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt index 0fb397effb..22ead70b9b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt @@ -79,7 +79,7 @@ internal class SwapAlertFactory @Inject constructor( derivationPath = cryptoCurrency?.network?.derivationPath?.value.orEmpty(), destinationAddress = confirmData?.enteredDestination.orEmpty(), tokenSymbol = confirmData?.toCryptoCurrencyStatus?.currency?.symbol.orEmpty(), - amount = confirmData?.enteredAmount?.toString().orEmpty(), + amount = confirmData?.enteredFromAmount?.toString().orEmpty(), fee = confirmData?.fee?.amount?.value?.toString() .orEmpty(), ), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt index b07b87a6ac..552956a5b9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/SwapQuoteUM.kt @@ -38,6 +38,7 @@ internal sealed class SwapQuoteUM { val diffPercent: DifferencePercent, val isSingleProvider: Boolean, val rate: TextReference, + val quoteId: String? = null, ) : SwapQuoteUM() { sealed class DifferencePercent { data object Empty : DifferencePercent() diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt index 3634569cf9..db6c8d8dbe 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt @@ -9,6 +9,7 @@ import com.tangem.domain.express.models.ExpressError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId +import java.math.BigDecimal import com.tangem.features.swap.v2.impl.notifications.model.SwapNotificationsModel import com.tangem.features.swap.v2.impl.notifications.ui.swapNotifications import kotlinx.collections.immutable.ImmutableList @@ -46,6 +47,8 @@ internal class SwapNotificationsComponent( val memo: String? = null, val toCryptoCurrencyStatus: CryptoCurrencyStatus? = null, val userWalletId: UserWalletId? = null, + val enteredFromAmount: BigDecimal? = null, + val fromCryptoCurrencyStatus: CryptoCurrencyStatus? = null, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt index eadc69c82c..fc14191b9b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt @@ -51,6 +51,11 @@ internal object SwapNotificationUM { subtitle = resourceReference(R.string.warning_express_providers_fca_warning_description), iconResId = R.drawable.ic_alert_circle_24, ) + + data object InsufficientFunds : Error( + title = resourceReference(R.string.swapping_insufficient_funds), + subtitle = resourceReference(R.string.swapping_insufficient_funds_description), + ) } sealed class Warning( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt index 89e743866c..73dc591264 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt @@ -65,6 +65,7 @@ internal class SwapNotificationsModel @Inject constructor( private suspend fun buildNotifications() { val notifications = buildList { + addInsufficientFundsNotification() addExpressErrorNotification() addDestinationTagRequiredNotification() } @@ -93,6 +94,14 @@ internal class SwapNotificationsModel @Inject constructor( } } + private fun MutableList.addInsufficientFundsNotification() { + val enteredFromAmount = notificationData.enteredFromAmount ?: return + val balance = notificationData.fromCryptoCurrencyStatus?.value?.amount ?: return + if (enteredFromAmount > balance) { + add(SwapNotificationUM.Error.InsufficientFunds) + } + } + fun MutableList.addExpressErrorNotification() { val expressError = notificationData.expressError ?: return val fromCryptoCurrency = notificationData.fromCryptoCurrency ?: return diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index 59d34c0701..98b57000b7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -117,7 +117,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( is CryptoCurrency.Coin -> "0" }, memo = null, - amountValue = model.confirmData.enteredAmount.orZero(), + amountValue = model.confirmData.enteredFromAmount.orZero(), reduceAmountBy = model.confirmData.reduceAmountBy.orZero(), isIgnoreReduce = model.confirmData.isIgnoreReduce, fee = model.confirmData.fee, @@ -137,6 +137,8 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( memo = model.confirmData.enteredMemo, toCryptoCurrencyStatus = model.confirmData.toCryptoCurrencyStatus, userWalletId = params.userWallet.walletId, + enteredFromAmount = model.confirmData.enteredFromAmount, + fromCryptoCurrencyStatus = model.confirmData.fromCryptoCurrencyStatus, ), ), ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index bea5544651..3c685ed2c6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -138,12 +138,18 @@ internal class SendWithSwapConfirmModel @Inject constructor( onDirect = { amountUM.primaryAmount }, onReverse = { amountUM.secondaryAmount }, ) - val amountState = fromAmount?.amountField as? AmountState.Data + val toAmount = amountUM?.swapDirection?.withSwapDirection( + onDirect = { amountUM.secondaryAmount }, + onReverse = { amountUM.primaryAmount }, + ) + val amountStateFrom = fromAmount?.amountField as? AmountState.Data + val amountStateTo = toAmount?.amountField as? AmountState.Data val isQuoteContent = amountUM?.selectedQuote is SwapQuoteUM.Content return ConfirmData( - enteredAmount = amountState?.amountTextField?.cryptoAmount?.value, - reduceAmountBy = amountState?.reduceAmountBy.takeIf { isQuoteContent }.orZero(), - isIgnoreReduce = amountState?.isIgnoreReduce == true, + enteredToAmount = amountStateTo?.amountTextField?.cryptoAmount?.value, + enteredFromAmount = amountStateFrom?.amountTextField?.cryptoAmount?.value, + reduceAmountBy = amountStateFrom?.reduceAmountBy.takeIf { isQuoteContent }.orZero(), + isIgnoreReduce = amountStateFrom?.isIgnoreReduce == true, enteredDestination = destinationUM?.addressTextField?.actualAddress, enteredMemo = destinationUM?.memoTextField?.value, fee = feeSelectorUM?.selectedFeeItem?.fee.takeIf { isQuoteContent }, @@ -250,7 +256,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( val defaultError = GetFeeError.UnknownError.left() val provider = (confirmData.quote as? SwapQuoteUM.Content)?.provider ?: return defaultError - val amountValue = confirmData.enteredAmount ?: return defaultError + val amountValue = confirmData.enteredFromAmount ?: return defaultError return when (val providerType = provider.type) { ExpressProviderType.CEX -> { @@ -274,7 +280,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( suspend fun loadFeeExtended(maybeToken: CryptoCurrencyStatus?): Either { val defaultError = GetFeeError.UnknownError.left() val provider = (confirmData.quote as? SwapQuoteUM.Content)?.provider ?: return defaultError - val amountValue = confirmData.enteredAmount ?: return defaultError + val amountValue = confirmData.enteredFromAmount ?: return defaultError return when (val providerType = provider.type) { ExpressProviderType.CEX -> { @@ -420,7 +426,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( is CryptoCurrency.Coin -> "0" }, memo = null, - amountValue = confirmData.enteredAmount.orZero(), + amountValue = confirmData.enteredFromAmount.orZero(), reduceAmountBy = confirmData.reduceAmountBy, isIgnoreReduce = confirmData.isIgnoreReduce, fee = confirmData.fee, @@ -436,6 +442,8 @@ internal class SendWithSwapConfirmModel @Inject constructor( memo = confirmData.enteredMemo, toCryptoCurrencyStatus = confirmData.toCryptoCurrencyStatus, userWalletId = params.userWallet.walletId, + enteredFromAmount = confirmData.enteredFromAmount, + fromCryptoCurrencyStatus = confirmData.fromCryptoCurrencyStatus, ), ) uiState.transformerUpdate( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt index b5c91be315..080ab91574 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt @@ -25,6 +25,7 @@ import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransac import com.tangem.domain.utils.convertToSdkAmount import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils import com.tangem.features.swap.v2.impl.common.ConfirmData +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -87,33 +88,43 @@ internal class SwapTransactionSender @AssistedInject constructor( val fromAccount = confirmData.fromAccount val provider = confirmData.quote?.provider ?: return val rateType = confirmData.rateType ?: return - val amountValue = confirmData.enteredAmount ?: return val feeValue = confirmData.fee?.amount?.value ?: return val destination = confirmData.enteredDestination ?: return - val fromAmount = FeeCalculationUtils.checkAndCalculateSubtractedAmount( - isAmountSubtractAvailable = isAmountSubtractAvailable, - cryptoCurrencyStatus = fromStatus, - amountValue = amountValue, - feeValue = feeValue, - reduceAmountBy = confirmData.reduceAmountBy, - ) + val (amount, currencyStatus) = when (confirmData.amountType) { + SwapAmountType.From -> { + val amountValue = confirmData.enteredFromAmount ?: return + val subtracted = FeeCalculationUtils.checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isAmountSubtractAvailable, + cryptoCurrencyStatus = fromStatus, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = confirmData.reduceAmountBy, + ) + subtracted to fromStatus + } + SwapAmountType.To -> { + val amountValue = confirmData.enteredToAmount ?: return + amountValue to toStatus + } + } val swapData = getSwapDataUseCase( userWallet = userWallet, fromCryptoCurrencyStatus = fromStatus, - amount = fromAmount.toStringWithRightOffset(fromStatus.currency.decimals), - amountType = SwapAmountType.From, + amount = amount.toStringWithRightOffset(currencyStatus.currency.decimals), + amountType = confirmData.amountType, toCryptoCurrency = toStatus.currency, toAddress = destination, toExtraId = confirmData.enteredMemo, expressProvider = provider, rateType = rateType, expressOperationType = expressOperationType, + quoteId = (confirmData.quote as? SwapQuoteUM.Content)?.quoteId, ).getOrElse { error -> onExpressError(error); return } createAndSendCexTransaction( - fromAmount = fromAmount, + fromAmount = amount, fromStatus = fromStatus, fromAccount = fromAccount, toStatus = toStatus, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SendWithSwapRateInfoFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SendWithSwapRateInfoFactory.kt new file mode 100644 index 0000000000..29b381307f --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SendWithSwapRateInfoFactory.kt @@ -0,0 +1,33 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.rateinfo + +import com.tangem.core.ui.components.bottomsheets.message.* +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.express.models.ExpressRateType +import com.tangem.features.swap.v2.impl.R + +internal object SendWithSwapRateInfoFactory { + + fun getRateTypeMessage(expressRateType: ExpressRateType, onDismiss: () -> Unit): MessageBottomSheetUM { + val isFixed = expressRateType == ExpressRateType.Fixed + return messageBottomSheetUM { + onDismiss(onDismiss) + infoBlock { + iconImage(if (isFixed) R.drawable.ic_fixed_32 else R.drawable.ic_floating_32) + title = resourceReference( + if (isFixed) R.string.send_rate_fixed_info_title else R.string.send_rate_floating_info_title, + ) + body = resourceReference( + if (isFixed) { + R.string.send_rate_fixed_info_description + } else { + R.string.send_rate_floating_info_description + }, + ) + } + secondaryButton { + text = resourceReference(R.string.common_got_it) + onClick { closeBs() } + } + } + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SwapRateInfoComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SwapRateInfoComponent.kt new file mode 100644 index 0000000000..f7f7cb6c6f --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/rateinfo/SwapRateInfoComponent.kt @@ -0,0 +1,27 @@ +package com.tangem.features.swap.v2.impl.sendviaswap.rateinfo + +import androidx.compose.runtime.Composable +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheet +import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUM +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.express.models.ExpressRateType + +internal class SwapRateInfoComponent( + appComponentContext: AppComponentContext, + expressRateType: ExpressRateType, + private val onDismiss: () -> Unit, +) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { + + private val state: MessageBottomSheetUM = SendWithSwapRateInfoFactory.getRateTypeMessage( + expressRateType = expressRateType, + onDismiss = onDismiss, + ) + + override fun dismiss() = onDismiss() + + @Composable + override fun BottomSheet() { + MessageBottomSheet(state = state, onDismissRequest = ::dismiss) + } +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index ebd13fd35b..968145424e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -344,6 +344,7 @@ internal class DefaultSwapRepository( appPreferencesStore = appPreferencesStore, ), toExtraId = toExtraId?.ifEmpty { null }, + quoteId = null, ).getOrThrow() if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) { val txDetails = parseTxDetails(response.txDetailsJson) From b74acabcc4e1d85c8b904ce351e90d9df5bf20f6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Mar 2026 12:17:28 +0200 Subject: [PATCH 13/75] Updated on 2026-08-14 --- .../api/tangemTech/TangemTechApi.kt | 6 +-- .../PromoBannerDisplaysResponse.kt | 33 ++++++++---- .../analytics/PromoBannerAnalyticsEvent.kt | 33 +++++++++--- .../PromoBannerDisplayDTOConverter.kt | 2 +- ...omoBannerDisplayToNotificationConverter.kt | 2 +- .../impl/model/PromoBannerDisplay.kt | 2 +- .../impl/model/PromoBannerNotificationUM.kt | 2 +- .../impl/model/PromoBannerPriority.kt | 2 +- .../impl/model/PromoBannersBlockModel.kt | 53 +++++++++++++------ .../impl/model/PromoBannersBlockUM.kt | 10 ++-- .../DefaultPromoBannersRepository.kt | 6 +-- .../impl/repository/PromoBannersRepository.kt | 8 ++- .../promobanners/impl/ui/PromoBannersBlock.kt | 29 ++++++---- .../PromoBannerDisplayDTOConverterTest.kt | 6 +-- ...annerDisplayToNotificationConverterTest.kt | 10 ++-- 15 files changed, 136 insertions(+), 68 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 739514fe49..6d851c0067 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -191,12 +191,12 @@ interface TangemTechApi { suspend fun getPromoBannerDisplays( @Query("walletId") walletId: String, @Query("placeholder") placeholder: String, - @Query("locale") locale: String, + @Query("lang") languageISOCode: String, ): ApiResponse - @PATCH("v1/displays/{displayId}") + @PATCH("v1/banner/displays/{displayId}") suspend fun dismissPromoBannerDisplay( - @Path("displayId") displayId: String, + @Path("displayId") displayId: Int, @Body body: DismissPromoBannerRequest, ): ApiResponse // endregion diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/promobanners/PromoBannerDisplaysResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/promobanners/PromoBannerDisplaysResponse.kt index ca1a8e9e5e..bc69dbf9c8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/promobanners/PromoBannerDisplaysResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/promobanners/PromoBannerDisplaysResponse.kt @@ -5,20 +5,31 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class PromoBannerDisplaysResponse( - @Json(name = "items") val items: List, + @Json(name = "items") + val items: List, ) @Suppress("BooleanPropertyNaming") @JsonClass(generateAdapter = true) data class PromoBannerDisplayDTO( - @Json(name = "id") val id: String, - @Json(name = "placeholder") val placeholder: String, - @Json(name = "priority") val priority: String, - @Json(name = "title") val title: String, - @Json(name = "subtitle") val subtitle: String, - @Json(name = "iconUrl") val iconUrl: String?, - @Json(name = "deeplink") val deeplink: String?, - @Json(name = "buttonEnabled") val buttonEnabled: Boolean, - @Json(name = "buttonText") val buttonText: String?, - @Json(name = "dismissable") val dismissable: Boolean, + @Json(name = "id") + val id: Int, + @Json(name = "placeholder") + val placeholder: String, + @Json(name = "priority") + val priority: String, + @Json(name = "title") + val title: String, + @Json(name = "subtitle") + val subtitle: String, + @Json(name = "iconUrl") + val iconUrl: String?, + @Json(name = "deeplink") + val deeplink: String?, + @Json(name = "buttonEnabled") + val buttonEnabled: Boolean, + @Json(name = "buttonText") + val buttonText: String?, + @Json(name = "dismissable") + val dismissable: Boolean, ) \ No newline at end of file diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/analytics/PromoBannerAnalyticsEvent.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/analytics/PromoBannerAnalyticsEvent.kt index 229e06fcdb..d6ca0f6001 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/analytics/PromoBannerAnalyticsEvent.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/analytics/PromoBannerAnalyticsEvent.kt @@ -8,34 +8,51 @@ internal sealed class PromoBannerAnalyticsEvent( ) : AnalyticsEvent(category = "Promo Banner", event = event, params = params) { data class Shown( - private val displayId: String, + private val displayId: Int, private val placeholder: String, ) : PromoBannerAnalyticsEvent( event = "Banner Shown", - params = mapOf("Display Id" to displayId, "Placeholder" to placeholder), + params = mapOf( + PARAM_DISPLAY_ID to displayId.toString(), + PARAM_PLACEHOLDER to placeholder, + ), ) data class CarouselScrolled( - private val displayId: String, + private val displayId: Int, private val placeholder: String, ) : PromoBannerAnalyticsEvent( event = "Banner Carousel Scrolled", - params = mapOf("Display Id" to displayId, "Placeholder" to placeholder), + params = mapOf( + PARAM_DISPLAY_ID to displayId.toString(), + PARAM_PLACEHOLDER to placeholder, + ), ) data class Clicked( - private val displayId: String, + private val displayId: Int, private val placeholder: String, ) : PromoBannerAnalyticsEvent( event = "Banner Button Clicked", - params = mapOf("Display Id" to displayId, "Placeholder" to placeholder), + params = mapOf( + PARAM_DISPLAY_ID to displayId.toString(), + PARAM_PLACEHOLDER to placeholder, + ), ) data class Dismissed( - private val displayId: String, + private val displayId: Int, private val placeholder: String, ) : PromoBannerAnalyticsEvent( event = "Banner Dismissed", - params = mapOf("Display Id" to displayId, "Placeholder" to placeholder), + params = mapOf( + PARAM_DISPLAY_ID to displayId.toString(), + PARAM_PLACEHOLDER to placeholder, + ), ) + + private companion object { + const val PARAM_DISPLAY_ID = "Display Id" + const val PARAM_PLACEHOLDER = "Placeholder" + } } \ No newline at end of file diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayDTOConverter.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayDTOConverter.kt index 13e19d5361..ef32e2291c 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayDTOConverter.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayDTOConverter.kt @@ -25,7 +25,7 @@ internal class PromoBannerDisplayDTOConverter : Converter PromoBannerPriority.IMPORTANT + "TOP" -> PromoBannerPriority.TOP "HIGH" -> PromoBannerPriority.HIGH "MEDIUM" -> PromoBannerPriority.MEDIUM "LOW" -> PromoBannerPriority.LOW diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayToNotificationConverter.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayToNotificationConverter.kt index 0e81007163..91c29f151a 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayToNotificationConverter.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayToNotificationConverter.kt @@ -10,7 +10,7 @@ internal class PromoBannerDisplayToNotificationConverter { fun convert( banner: PromoBannerDisplay, onDeeplinkClick: (String?) -> Unit, - onDismiss: (String) -> Unit, + onDismiss: (Int) -> Unit, ): PromoBannerNotificationUM { return PromoBannerNotificationUM( displayId = banner.id, diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerDisplay.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerDisplay.kt index 6f96a1cf00..15c278cee2 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerDisplay.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerDisplay.kt @@ -1,7 +1,7 @@ package com.tangem.features.promobanners.impl.model internal data class PromoBannerDisplay( - val id: String, + val id: Int, val placeholder: String, val priority: PromoBannerPriority, val title: String, diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerNotificationUM.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerNotificationUM.kt index 1d244d419c..6003b4e54f 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerNotificationUM.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerNotificationUM.kt @@ -3,6 +3,6 @@ package com.tangem.features.promobanners.impl.model import com.tangem.core.ui.components.notifications.NotificationConfig internal data class PromoBannerNotificationUM( - val displayId: String, + val displayId: Int, val config: NotificationConfig, ) \ No newline at end of file diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerPriority.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerPriority.kt index 959f3c3b44..bdb0403226 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerPriority.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannerPriority.kt @@ -1,7 +1,7 @@ package com.tangem.features.promobanners.impl.model internal enum class PromoBannerPriority(val order: Int) { - IMPORTANT(order = 0), + TOP(order = 0), HIGH(order = 1), MEDIUM(order = 2), LOW(order = 3), diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt index 5c596f3fd4..f4f2d505e5 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockModel.kt @@ -12,6 +12,7 @@ import com.tangem.features.promobanners.impl.converters.PromoBannerDisplayToNoti import com.tangem.features.promobanners.impl.repository.PromoBannersRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching +import kotlinx.collections.immutable.persistentListOf import com.tangem.utils.logging.TangemLogger import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* @@ -34,11 +35,12 @@ internal class PromoBannersBlockModel @Inject constructor( private val converter = PromoBannerDisplayToNotificationConverter() private val placeholder: String = params.placeholder.name.lowercase() - private val shownBannerIds: MutableSet = ConcurrentHashMap.newKeySet() + private val shownBannerIds: MutableSet = ConcurrentHashMap.newKeySet() private var wasCarouselScrolled = false + private val savedDisplayIdByWalletId: MutableMap = mutableMapOf() val uiState: StateFlow - field = MutableStateFlow(PromoBannersBlockUM()) + field = MutableStateFlow(getInitialState()) init { subscribeOnSelectedWallet() @@ -59,46 +61,67 @@ internal class PromoBannersBlockModel @Inject constructor( } private suspend fun loadBanners(walletId: String) { - val locale = Locale.getDefault().language + val languageISOCode = Locale.getDefault().language runSuspendCatching { - repository.getBanners(walletId, params.placeholder, locale) + repository.getBanners(walletId, params.placeholder, languageISOCode) }.onSuccess { banners -> + val bannerUMs = banners.map { banner -> + converter.convert( + banner = banner, + onDeeplinkClick = { deeplink -> onButtonClick(banner.id, deeplink) }, + onDismiss = { displayId -> onBannerDismiss(walletId, displayId) }, + ) + }.toImmutableList() + + val savedDisplayId = savedDisplayIdByWalletId[walletId] + val initialPage = if (savedDisplayId != null) { + bannerUMs.indexOfFirst { it.displayId == savedDisplayId }.coerceAtLeast(0) + } else { + 0 + } + uiState.value = PromoBannersBlockUM( - banners = banners.map { banner -> - converter.convert( - banner = banner, - onDeeplinkClick = { deeplink -> onButtonClick(banner.id, deeplink) }, - onDismiss = { displayId -> onBannerDismiss(walletId, displayId) }, - ) - }.toImmutableList(), + userWalletId = walletId, + initialPage = initialPage, + banners = bannerUMs, onBannerShown = ::onBannerShown, onCarouselScrolled = ::onCarouselScrolled, + onPageChanged = { displayId -> savedDisplayIdByWalletId[walletId] = displayId }, ) }.onFailure { error -> TangemLogger.w("Failed to load promo banners", error) } } - private fun onBannerShown(displayId: String) { + private fun onBannerShown(displayId: Int) { if (shownBannerIds.add(displayId)) { analyticsEventHandler.send(PromoBannerAnalyticsEvent.Shown(displayId, placeholder)) } } - private fun onCarouselScrolled(displayId: String) { + private fun onCarouselScrolled(displayId: Int) { if (!wasCarouselScrolled) { wasCarouselScrolled = true analyticsEventHandler.send(PromoBannerAnalyticsEvent.CarouselScrolled(displayId, placeholder)) } } - private fun onButtonClick(displayId: String, deeplink: String?) { + private fun onButtonClick(displayId: Int, deeplink: String?) { analyticsEventHandler.send(PromoBannerAnalyticsEvent.Clicked(displayId, placeholder)) deeplink?.let { deeplinkLauncher.launch(it) } } - private fun onBannerDismiss(walletId: String, displayId: String) { + private fun getInitialState() = PromoBannersBlockUM( + userWalletId = "", + initialPage = 0, + banners = persistentListOf(), + onBannerShown = {}, + onCarouselScrolled = {}, + onPageChanged = {}, + ) + + private fun onBannerDismiss(walletId: String, displayId: Int) { analyticsEventHandler.send(PromoBannerAnalyticsEvent.Dismissed(displayId, placeholder)) uiState.update { state -> state.copy( diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockUM.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockUM.kt index e58b9d432c..0b12f8cf7c 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockUM.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/model/PromoBannersBlockUM.kt @@ -1,10 +1,12 @@ package com.tangem.features.promobanners.impl.model import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf internal data class PromoBannersBlockUM( - val banners: ImmutableList = persistentListOf(), - val onBannerShown: (displayId: String) -> Unit = {}, - val onCarouselScrolled: (displayId: String) -> Unit = {}, + val userWalletId: String, + val initialPage: Int, + val banners: ImmutableList, + val onBannerShown: (displayId: Int) -> Unit, + val onCarouselScrolled: (displayId: Int) -> Unit, + val onPageChanged: (displayId: Int) -> Unit, ) \ No newline at end of file diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/DefaultPromoBannersRepository.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/DefaultPromoBannersRepository.kt index ba193f57e4..9a13d9fab1 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/DefaultPromoBannersRepository.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/DefaultPromoBannersRepository.kt @@ -28,7 +28,7 @@ internal class DefaultPromoBannersRepository( override suspend fun getBanners( walletId: String, placeholder: Placeholder, - locale: String, + languageISOCode: String, ): List { val key = BannersCacheKey(walletId, placeholder) @@ -38,7 +38,7 @@ internal class DefaultPromoBannersRepository( tangemTechApi.getPromoBannerDisplays( walletId = walletId, placeholder = placeholder.toApiValue(), - locale = locale, + languageISOCode = languageISOCode, ).getOrThrow() .items .map(converter::convert) @@ -52,7 +52,7 @@ internal class DefaultPromoBannersRepository( return banners } - override suspend fun dismissBanner(walletId: String, displayId: String) { + override suspend fun dismissBanner(walletId: String, displayId: Int) { cache.update(default = emptyMap()) { current -> current.mapValues { (key, banners) -> if (key.walletId == walletId) { diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/PromoBannersRepository.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/PromoBannersRepository.kt index e4e7430d98..0fd96683b7 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/PromoBannersRepository.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/repository/PromoBannersRepository.kt @@ -5,7 +5,11 @@ import com.tangem.features.promobanners.impl.model.PromoBannerDisplay internal interface PromoBannersRepository { - suspend fun getBanners(walletId: String, placeholder: Placeholder, locale: String): List + suspend fun getBanners( + walletId: String, + placeholder: Placeholder, + languageISOCode: String, + ): List - suspend fun dismissBanner(walletId: String, displayId: String) + suspend fun dismissBanner(walletId: String, displayId: Int) } \ No newline at end of file diff --git a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/ui/PromoBannersBlock.kt b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/ui/PromoBannersBlock.kt index 37b834d756..49cff50a0a 100644 --- a/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/ui/PromoBannersBlock.kt +++ b/features/promo-banners/impl/src/main/kotlin/com/tangem/features/promobanners/impl/ui/PromoBannersBlock.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.key import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,12 +36,16 @@ internal fun PromoBannersBlock(state: PromoBannersBlockUM, modifier: Modifier = modifier = modifier, ) } else { - BannersCarousel( - banners = state.banners, - onBannerShown = state.onBannerShown, - onCarouselScroll = state.onCarouselScrolled, - modifier = modifier, - ) + key(state.userWalletId) { + BannersCarousel( + banners = state.banners, + initialPage = state.initialPage, + onBannerShown = state.onBannerShown, + onCarouselScroll = state.onCarouselScrolled, + onPageChange = state.onPageChanged, + modifier = modifier, + ) + } } } @@ -56,16 +61,22 @@ private fun SingleBanner(banner: PromoBannerNotificationUM, modifier: Modifier = @Composable private fun BannersCarousel( banners: List, - onBannerShown: (String) -> Unit, - onCarouselScroll: (String) -> Unit, + initialPage: Int, + onBannerShown: (Int) -> Unit, + onCarouselScroll: (Int) -> Unit, + onPageChange: (Int) -> Unit, modifier: Modifier = Modifier, ) { - val pagerState = rememberPagerState(pageCount = { banners.size }) + val pagerState = rememberPagerState( + initialPage = initialPage, + pageCount = { banners.size }, + ) LaunchedEffect(pagerState, banners) { snapshotFlow { pagerState.currentPage } .collect { page -> banners.getOrNull(page)?.let { banner -> + onPageChange(banner.displayId) onBannerShown(banner.displayId) if (page == 1) { // one-time event when user scrolls from first to second page, diff --git a/features/promo-banners/impl/src/test/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayDTOConverterTest.kt b/features/promo-banners/impl/src/test/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayDTOConverterTest.kt index 91eb08c41a..36d87c263e 100644 --- a/features/promo-banners/impl/src/test/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayDTOConverterTest.kt +++ b/features/promo-banners/impl/src/test/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayDTOConverterTest.kt @@ -12,7 +12,7 @@ class PromoBannerDisplayDTOConverterTest { @Test fun `should convert DTO to domain model`() { val dto = PromoBannerDisplayDTO( - id = "123", + id = 123, placeholder = "MAIN", priority = "HIGH", title = "Test Banner", @@ -26,7 +26,7 @@ class PromoBannerDisplayDTOConverterTest { val result = converter.convert(dto) - assertThat(result.id).isEqualTo("123") + assertThat(result.id).isEqualTo(123) assertThat(result.placeholder).isEqualTo("MAIN") assertThat(result.priority).isEqualTo(PromoBannerPriority.HIGH) assertThat(result.title).isEqualTo("Test Banner") @@ -63,7 +63,7 @@ class PromoBannerDisplayDTOConverterTest { placeholder: String = "MAIN", priority: String = "MEDIUM", ) = PromoBannerDisplayDTO( - id = "1", + id = 1, placeholder = placeholder, priority = priority, title = "Title", diff --git a/features/promo-banners/impl/src/test/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayToNotificationConverterTest.kt b/features/promo-banners/impl/src/test/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayToNotificationConverterTest.kt index fed421aa20..fdae92bc97 100644 --- a/features/promo-banners/impl/src/test/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayToNotificationConverterTest.kt +++ b/features/promo-banners/impl/src/test/kotlin/com/tangem/features/promobanners/impl/converters/PromoBannerDisplayToNotificationConverterTest.kt @@ -13,14 +13,14 @@ class PromoBannerDisplayToNotificationConverterTest { @Test fun `should convert banner with all fields`() { val banner = createBanner( - id = "b1", + id = 1, deeplink = "tangem://wallet", isButtonEnabled = true, buttonText = "Open", isDismissable = true, ) var clickedDeeplink: String? = "not_called" - var dismissedId: String? = null + var dismissedId: Int? = null val result = converter.convert( banner = banner, @@ -28,7 +28,7 @@ class PromoBannerDisplayToNotificationConverterTest { onDismiss = { dismissedId = it }, ) - assertThat(result.displayId).isEqualTo("b1") + assertThat(result.displayId).isEqualTo(1) assertThat(result.config.title).isEqualTo(TextReference.Str("Title")) assertThat(result.config.subtitle).isEqualTo(TextReference.Str("Subtitle")) assertThat(result.config.iconUrl).isEqualTo("https://icon.png") @@ -42,7 +42,7 @@ class PromoBannerDisplayToNotificationConverterTest { assertThat(clickedDeeplink).isEqualTo("tangem://wallet") result.config.onCloseClick!!.invoke() - assertThat(dismissedId).isEqualTo("b1") + assertThat(dismissedId).isEqualTo(1) } @Test @@ -98,7 +98,7 @@ class PromoBannerDisplayToNotificationConverterTest { } private fun createBanner( - id: String = "id", + id: Int = 0, deeplink: String? = "https://example.com", isButtonEnabled: Boolean = false, buttonText: String? = null, From 4649abbb600e944dbf4c82bf2823088848b7bc13 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Mar 2026 13:11:50 +0100 Subject: [PATCH 14/75] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + data/search/.gitignore | 1 + data/search/build.gradle.kts | 40 ++++++ .../converter/SearchHistoryConverter.kt | 41 ++++++ .../tangem/data/search/di/SearchDataModule.kt | 98 ++++++++++++++ .../tangem/data/search/model/SearchHistory.kt | 24 ++++ .../repository/DefaultSearchRepository.kt | 60 +++++++++ .../search/store/DefaultSearchHistoryStore.kt | 53 ++++++++ .../data/search/store/SearchHistoryStore.kt | 13 ++ domain/search/.gitignore | 1 + domain/search/build.gradle.kts | 20 +++ .../domain/search/model/RecentSearchToken.kt | 14 ++ .../domain/search/model/SearchResult.kt | 10 ++ .../domain/search/model/SearchTextHint.kt | 6 + .../search/model/UserAssetSearchEntry.kt | 14 ++ .../search/repository/SearchRepository.kt | 39 ++++++ .../usecase/ClearSearchHistoryUseCase.kt | 17 +++ .../search/usecase/GetSearchResultsUseCase.kt | 126 ++++++++++++++++++ .../usecase/SaveRecentSearchTokenUseCase.kt | 23 ++++ .../search/usecase/SaveSearchQueryUseCase.kt | 23 ++++ settings.gradle.kts | 2 + 21 files changed, 627 insertions(+) create mode 100644 data/search/.gitignore create mode 100644 data/search/build.gradle.kts create mode 100644 data/search/src/main/java/com/tangem/data/search/converter/SearchHistoryConverter.kt create mode 100644 data/search/src/main/java/com/tangem/data/search/di/SearchDataModule.kt create mode 100644 data/search/src/main/java/com/tangem/data/search/model/SearchHistory.kt create mode 100644 data/search/src/main/java/com/tangem/data/search/repository/DefaultSearchRepository.kt create mode 100644 data/search/src/main/java/com/tangem/data/search/store/DefaultSearchHistoryStore.kt create mode 100644 data/search/src/main/java/com/tangem/data/search/store/SearchHistoryStore.kt create mode 100644 domain/search/.gitignore create mode 100644 domain/search/build.gradle.kts create mode 100644 domain/search/src/main/java/com/tangem/domain/search/model/RecentSearchToken.kt create mode 100644 domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt create mode 100644 domain/search/src/main/java/com/tangem/domain/search/model/SearchTextHint.kt create mode 100644 domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt create mode 100644 domain/search/src/main/java/com/tangem/domain/search/repository/SearchRepository.kt create mode 100644 domain/search/src/main/java/com/tangem/domain/search/usecase/ClearSearchHistoryUseCase.kt create mode 100644 domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt create mode 100644 domain/search/src/main/java/com/tangem/domain/search/usecase/SaveRecentSearchTokenUseCase.kt create mode 100644 domain/search/src/main/java/com/tangem/domain/search/usecase/SaveSearchQueryUseCase.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5379cdff1d..bb907bd129 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -161,6 +161,7 @@ dependencies { implementation(projects.domain.news) implementation(projects.domain.earn) implementation(projects.domain.tokensync) + implementation(projects.domain.search) implementation(projects.common) implementation(projects.common.routing) @@ -216,6 +217,7 @@ dependencies { implementation(projects.data.hotWallet) implementation(projects.data.news) implementation(projects.data.earn) + implementation(projects.data.search) /** Features */ implementation(projects.features.referral.impl) diff --git a/data/search/.gitignore b/data/search/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/search/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/search/build.gradle.kts b/data/search/build.gradle.kts new file mode 100644 index 0000000000..96eb497c11 --- /dev/null +++ b/data/search/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.search" +} + +dependencies { + // region Project - Core + implementation(projects.core.datasource) + api(projects.core.utils) + // endregion + + // region Project - Data + implementation(projects.data.common) + // endregion + + // region Project - Domain + implementation(projects.domain.search) + implementation(projects.domain.common) + implementation(projects.domain.account.status) + implementation(projects.domain.markets.models) + implementation(projects.domain.wallets) + implementation(projects.domain.appCurrency) + // endregion + + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // endregion + + // region Other libraries + implementation(deps.androidx.datastore) + implementation(deps.moshi.kotlin) + // endregion +} \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/converter/SearchHistoryConverter.kt b/data/search/src/main/java/com/tangem/data/search/converter/SearchHistoryConverter.kt new file mode 100644 index 0000000000..60bedf12bd --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/converter/SearchHistoryConverter.kt @@ -0,0 +1,41 @@ +package com.tangem.data.search.converter + +import com.tangem.data.search.model.RecentTokenDTO +import com.tangem.data.search.model.TextHintDTO +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.search.model.RecentSearchToken +import com.tangem.domain.search.model.SearchTextHint +import com.tangem.utils.converter.Converter + +internal class TextHintDTOToSearchTextHintConverter : Converter { + override fun convert(value: TextHintDTO): SearchTextHint { + return SearchTextHint( + text = value.text, + timestamp = value.timestamp, + ) + } +} + +internal class RecentTokenDTOToRecentSearchTokenConverter : Converter { + override fun convert(value: RecentTokenDTO): RecentSearchToken { + return RecentSearchToken( + id = CryptoCurrency.RawID(value.id), + name = value.name, + symbol = value.symbol, + imageUrl = value.imageUrl, + timestamp = value.timestamp, + ) + } +} + +internal class RecentSearchTokenToRecentTokenDTOConverter : Converter { + override fun convert(value: RecentSearchToken): RecentTokenDTO { + return RecentTokenDTO( + id = value.id.value, + name = value.name, + symbol = value.symbol, + imageUrl = value.imageUrl, + timestamp = value.timestamp, + ) + } +} \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/di/SearchDataModule.kt b/data/search/src/main/java/com/tangem/data/search/di/SearchDataModule.kt new file mode 100644 index 0000000000..e4a321d4da --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/di/SearchDataModule.kt @@ -0,0 +1,98 @@ +package com.tangem.data.search.di + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import com.tangem.data.search.model.SearchHistoryDTO +import com.tangem.data.search.repository.DefaultSearchRepository +import com.tangem.data.search.store.DefaultSearchHistoryStore +import com.tangem.data.search.store.SearchHistoryStore +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.search.repository.SearchRepository +import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase +import com.tangem.domain.search.usecase.GetSearchResultsUseCase +import com.tangem.domain.search.usecase.SaveRecentSearchTokenUseCase +import com.tangem.domain.search.usecase.SaveSearchQueryUseCase +import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object SearchDataModule { + + @OptIn(ExperimentalStdlibApi::class) + @Provides + @Singleton + fun provideSearchHistoryDataStore( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + appScope: AppCoroutineScope, + ): DataStore { + return DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + defaultValue = SearchHistoryDTO(), + adapter = moshi.adapter(), + ), + produceFile = { context.dataStoreFile(fileName = "search_history") }, + scope = appScope, + ) + } + + @Provides + @Singleton + fun provideSearchHistoryStore(dataStore: DataStore): SearchHistoryStore { + return DefaultSearchHistoryStore(dataStore = dataStore) + } + + @Provides + @Singleton + fun provideSearchRepository( + store: SearchHistoryStore, + dispatchers: CoroutineDispatcherProvider, + ): SearchRepository { + return DefaultSearchRepository( + store = store, + dispatchers = dispatchers, + ) + } + + @Provides + fun provideGetSearchResultsUseCase( + searchRepository: SearchRepository, + multiAccountStatusListSupplier: MultiAccountStatusListSupplier, + userWalletsListRepository: UserWalletsListRepository, + ): GetSearchResultsUseCase { + return GetSearchResultsUseCase( + searchRepository = searchRepository, + multiAccountStatusListSupplier = multiAccountStatusListSupplier, + userWalletsListRepository = userWalletsListRepository, + ) + } + + @Provides + fun provideSaveSearchQueryUseCase(searchRepository: SearchRepository): SaveSearchQueryUseCase { + return SaveSearchQueryUseCase(searchRepository = searchRepository) + } + + @Provides + fun provideSaveRecentSearchTokenUseCase(searchRepository: SearchRepository): SaveRecentSearchTokenUseCase { + return SaveRecentSearchTokenUseCase(searchRepository = searchRepository) + } + + @Provides + fun provideClearSearchHistoryUseCase(searchRepository: SearchRepository): ClearSearchHistoryUseCase { + return ClearSearchHistoryUseCase(searchRepository = searchRepository) + } +} \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/model/SearchHistory.kt b/data/search/src/main/java/com/tangem/data/search/model/SearchHistory.kt new file mode 100644 index 0000000000..3166dd4d71 --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/model/SearchHistory.kt @@ -0,0 +1,24 @@ +package com.tangem.data.search.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class SearchHistoryDTO( + val textHints: List = emptyList(), + val recentTokens: List = emptyList(), +) + +@JsonClass(generateAdapter = true) +internal data class TextHintDTO( + val text: String, + val timestamp: Long, +) + +@JsonClass(generateAdapter = true) +internal data class RecentTokenDTO( + val id: String, + val name: String, + val symbol: String, + val imageUrl: String?, + val timestamp: Long, +) \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/repository/DefaultSearchRepository.kt b/data/search/src/main/java/com/tangem/data/search/repository/DefaultSearchRepository.kt new file mode 100644 index 0000000000..def344a019 --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/repository/DefaultSearchRepository.kt @@ -0,0 +1,60 @@ +package com.tangem.data.search.repository + +import com.tangem.data.search.converter.RecentSearchTokenToRecentTokenDTOConverter +import com.tangem.data.search.converter.RecentTokenDTOToRecentSearchTokenConverter +import com.tangem.data.search.converter.TextHintDTOToSearchTextHintConverter +import com.tangem.data.search.model.TextHintDTO +import com.tangem.data.search.store.SearchHistoryStore +import com.tangem.domain.search.model.RecentSearchToken +import com.tangem.domain.search.model.SearchTextHint +import com.tangem.domain.search.repository.SearchRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +internal class DefaultSearchRepository( + private val store: SearchHistoryStore, + private val dispatchers: CoroutineDispatcherProvider, +) : SearchRepository { + + private val textHintConverter by lazy { + TextHintDTOToSearchTextHintConverter() + } + private val recentTokenConverter by lazy { + RecentTokenDTOToRecentSearchTokenConverter() + } + private val recentTokenDMConverter by lazy { + RecentSearchTokenToRecentTokenDTOConverter() + } + + override fun getTextHints(): Flow> { + return store.getTextHints() + .map { textHintConverter.convertList(it) } + .flowOn(dispatchers.io) + } + + override fun getRecentTokens(): Flow> { + return store.getRecentTokens() + .map { recentTokenConverter.convertList(it) } + .flowOn(dispatchers.io) + } + + override suspend fun saveTextHint(text: String) = withContext(dispatchers.io) { + store.saveTextHint( + TextHintDTO( + text = text, + timestamp = System.currentTimeMillis(), + ), + ) + } + + override suspend fun saveRecentToken(token: RecentSearchToken) = withContext(dispatchers.io) { + store.saveRecentToken(recentTokenDMConverter.convert(token)) + } + + override suspend fun clearHistory() = withContext(dispatchers.io) { + store.clearAll() + } +} \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/store/DefaultSearchHistoryStore.kt b/data/search/src/main/java/com/tangem/data/search/store/DefaultSearchHistoryStore.kt new file mode 100644 index 0000000000..348328faa1 --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/store/DefaultSearchHistoryStore.kt @@ -0,0 +1,53 @@ +package com.tangem.data.search.store + +import androidx.datastore.core.DataStore +import com.tangem.data.search.model.RecentTokenDTO +import com.tangem.data.search.model.SearchHistoryDTO +import com.tangem.data.search.model.TextHintDTO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal class DefaultSearchHistoryStore( + private val dataStore: DataStore, +) : SearchHistoryStore { + + override fun getTextHints(): Flow> { + return dataStore.data.map { it.textHints.sortedByDescending(TextHintDTO::timestamp) } + } + + override fun getRecentTokens(): Flow> { + return dataStore.data.map { it.recentTokens.sortedByDescending(RecentTokenDTO::timestamp) } + } + + override suspend fun saveTextHint(hint: TextHintDTO) { + dataStore.updateData { current -> + val updated = current.textHints + .filter { it.text != hint.text } + .toMutableList() + .apply { add(0, hint) } + .take(MAX_HISTORY_SIZE) + current.copy(textHints = updated) + } + } + + override suspend fun saveRecentToken(token: RecentTokenDTO) { + dataStore.updateData { current -> + val updated = current.recentTokens + .filter { it.id != token.id } + .toMutableList() + .apply { add(0, token) } + .take(MAX_HISTORY_SIZE) + current.copy(recentTokens = updated) + } + } + + override suspend fun clearAll() { + dataStore.updateData { + SearchHistoryDTO() + } + } + + private companion object { + const val MAX_HISTORY_SIZE = 3 + } +} \ No newline at end of file diff --git a/data/search/src/main/java/com/tangem/data/search/store/SearchHistoryStore.kt b/data/search/src/main/java/com/tangem/data/search/store/SearchHistoryStore.kt new file mode 100644 index 0000000000..02b398d0e5 --- /dev/null +++ b/data/search/src/main/java/com/tangem/data/search/store/SearchHistoryStore.kt @@ -0,0 +1,13 @@ +package com.tangem.data.search.store + +import com.tangem.data.search.model.RecentTokenDTO +import com.tangem.data.search.model.TextHintDTO +import kotlinx.coroutines.flow.Flow + +internal interface SearchHistoryStore { + fun getTextHints(): Flow> + fun getRecentTokens(): Flow> + suspend fun saveTextHint(hint: TextHintDTO) + suspend fun saveRecentToken(token: RecentTokenDTO) + suspend fun clearAll() +} \ No newline at end of file diff --git a/domain/search/.gitignore b/domain/search/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/search/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/search/build.gradle.kts b/domain/search/build.gradle.kts new file mode 100644 index 0000000000..e13e688d24 --- /dev/null +++ b/domain/search/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.search" +} + +dependencies { + api(projects.domain.core) + api(projects.domain.models) + implementation(projects.domain.common) + implementation(projects.domain.markets.models) + implementation(projects.domain.wallets) + implementation(projects.domain.appCurrency) + implementation(projects.domain.account) + implementation(projects.domain.account.status) +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/RecentSearchToken.kt b/domain/search/src/main/java/com/tangem/domain/search/model/RecentSearchToken.kt new file mode 100644 index 0000000000..d859f1d2a4 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/model/RecentSearchToken.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.search.model + +import com.tangem.domain.models.currency.CryptoCurrency + +/** + * @property timestamp epoch milliseconds + */ +data class RecentSearchToken( + val id: CryptoCurrency.RawID, + val name: String, + val symbol: String, + val imageUrl: String?, + val timestamp: Long, +) \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt b/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt new file mode 100644 index 0000000000..784f334bdb --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.search.model + +import com.tangem.domain.markets.TokenMarket + +data class SearchResult( + val textHints: List, + val recentTokens: List, + val userAssets: List, + val marketTokens: List, +) \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/SearchTextHint.kt b/domain/search/src/main/java/com/tangem/domain/search/model/SearchTextHint.kt new file mode 100644 index 0000000000..b949c8b3fa --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/model/SearchTextHint.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.search.model + +/** + * @property timestamp epoch milliseconds + */ +data class SearchTextHint(val text: String, val timestamp: Long) \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt b/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt new file mode 100644 index 0000000000..fd64a23e24 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.search.model + +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId + +data class UserAssetSearchEntry( + val userWalletId: UserWalletId, + val userWalletName: String, + val accountId: AccountId, + val accountName: AccountName, + val currencyStatus: CryptoCurrencyStatus, +) \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/repository/SearchRepository.kt b/domain/search/src/main/java/com/tangem/domain/search/repository/SearchRepository.kt new file mode 100644 index 0000000000..f7cea5e075 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/repository/SearchRepository.kt @@ -0,0 +1,39 @@ +package com.tangem.domain.search.repository + +import com.tangem.domain.search.model.RecentSearchToken +import com.tangem.domain.search.model.SearchTextHint +import kotlinx.coroutines.flow.Flow + +/** + * Repository responsible for managing local search history storage. + * Handles persistence of user's past search queries and recently viewed market tokens. + * Each history type is limited to 3 entries, sorted by timestamp in descending order. + */ +interface SearchRepository { + + /** Observes the list of saved text hints, sorted by timestamp descending. */ + fun getTextHints(): Flow> + + /** Observes the list of recently viewed market tokens, sorted by timestamp descending. */ + fun getRecentTokens(): Flow> + + /** + * Saves a text hint to the search history. + * If the hint already exists, its timestamp is updated. Oldest entries are evicted when the limit is exceeded. + * + * @param text the search query text to save + */ + suspend fun saveTextHint(text: String) + + /** + * Saves a recently viewed market token to the search history. + * If a token with the same ID already exists, it is moved to the top. Oldest entries are evicted when the limit + * is exceeded. + * + * @param token the market token entry to save + */ + suspend fun saveRecentToken(token: RecentSearchToken) + + /** Clears all search history, including both text hints and recent tokens. */ + suspend fun clearHistory() +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/usecase/ClearSearchHistoryUseCase.kt b/domain/search/src/main/java/com/tangem/domain/search/usecase/ClearSearchHistoryUseCase.kt new file mode 100644 index 0000000000..f12c208257 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/usecase/ClearSearchHistoryUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.search.usecase + +import com.tangem.domain.search.repository.SearchRepository + +/** + * Clears the entire search history, removing both text hints and recently viewed tokens. + * + * @property searchRepository local search history storage + */ +class ClearSearchHistoryUseCase( + private val searchRepository: SearchRepository, +) { + + suspend operator fun invoke() { + searchRepository.clearHistory() + } +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt b/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt new file mode 100644 index 0000000000..87e48a0215 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt @@ -0,0 +1,126 @@ +package com.tangem.domain.search.usecase + +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.markets.TokenMarket +import com.tangem.domain.models.account.filterCryptoPortfolio +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isLocked +import com.tangem.domain.search.model.SearchResult +import com.tangem.domain.search.model.UserAssetSearchEntry +import com.tangem.domain.search.repository.SearchRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf + +/** + * Primary search use case that produces [SearchResult] based on the current query. + * + * Behavior depends on the query: + * - **Empty query** — returns search history: text hints and recently viewed tokens. + * - **Non-empty query** — performs the search across all unlocked user wallets, + * matching currencies by name or symbol, and combines the results with externally provided market tokens. + * + * @property searchRepository local search history storage + * @property multiAccountStatusListSupplier supplier for loaded account status lists across all wallets + * @property userWalletsListRepository repository providing the list of user wallets + */ +class GetSearchResultsUseCase( + private val searchRepository: SearchRepository, + private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier, + private val userWalletsListRepository: UserWalletsListRepository, +) { + + /** + * Produces a [Flow] of [SearchResult] for the given [query]. + * + * @param query the search query string; blank means "show history" + * @param marketTokens external flow of market token search results (provided by presentation layer) + */ + operator fun invoke( + query: String, + marketTokens: Flow> = flowOf(emptyList()), + ): Flow { + return if (query.isBlank()) { + observeHistory() + } else { + searchAssets(query, marketTokens) + } + } + + private fun observeHistory(): Flow { + return combine( + searchRepository.getTextHints(), + searchRepository.getRecentTokens(), + ) { hints, tokens -> + SearchResult( + textHints = hints, + recentTokens = tokens, + userAssets = emptyList(), + marketTokens = emptyList(), + ) + } + } + + private fun searchAssets(query: String, marketTokens: Flow>): Flow { + return combine( + observeUserAssets(query), + marketTokens, + ) { userAssets, markets -> + SearchResult( + textHints = emptyList(), + recentTokens = emptyList(), + userAssets = userAssets, + marketTokens = markets, + ) + } + } + + private fun observeUserAssets(query: String): Flow> { + val lowerQuery = query.lowercase() + return combine( + multiAccountStatusListSupplier(), + userWalletsListRepository.userWallets, + ) { statusLists, wallets -> + val unlockedWallets = wallets + .orEmpty() + .filterNot(UserWallet::isLocked) + .associateBy { it.walletId } + + if (unlockedWallets.isEmpty()) return@combine emptyList() + + statusLists + .filter { it.userWalletId in unlockedWallets } + .flatMap { statusList -> extractMatchingAssets(statusList, unlockedWallets, lowerQuery) } + } + } + + private fun extractMatchingAssets( + statusList: AccountStatusList, + wallets: Map, + lowerQuery: String, + ): List { + val wallet = wallets[statusList.userWalletId] ?: return emptyList() + return statusList.accountStatuses + .filterCryptoPortfolio() + .flatMap { accountStatus -> + accountStatus.flattenCurrencies() + .filter { currencyStatus -> + val name = currencyStatus.currency.name.lowercase() + val symbol = currencyStatus.currency.symbol.lowercase() + name.contains(lowerQuery) || symbol.contains(lowerQuery) + } + .map { currencyStatus -> + UserAssetSearchEntry( + userWalletId = statusList.userWalletId, + userWalletName = wallet.name, + accountId = accountStatus.accountId, + accountName = accountStatus.account.accountName, + currencyStatus = currencyStatus, + ) + } + } + } +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveRecentSearchTokenUseCase.kt b/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveRecentSearchTokenUseCase.kt new file mode 100644 index 0000000000..9c8074fd7c --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveRecentSearchTokenUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.search.usecase + +import com.tangem.domain.search.model.RecentSearchToken +import com.tangem.domain.search.repository.SearchRepository + +/** + * Saves a market token to the "recently viewed" search history. + * Only market assets should be saved (user's own assets are ignored). + * The history is limited to 3 entries; oldest entries are evicted automatically. + * + * @property searchRepository local search history storage + */ +class SaveRecentSearchTokenUseCase( + private val searchRepository: SearchRepository, +) { + + /** + * @param token the market token to persist as a recent search entry + */ + suspend operator fun invoke(token: RecentSearchToken) { + searchRepository.saveRecentToken(token) + } +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveSearchQueryUseCase.kt b/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveSearchQueryUseCase.kt new file mode 100644 index 0000000000..486d9f40bd --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/usecase/SaveSearchQueryUseCase.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.search.usecase + +import com.tangem.domain.search.repository.SearchRepository + +/** + * Saves the current search query text to the local search history. + * Blank queries are ignored. The history is limited to 3 entries; oldest entries are evicted automatically. + * Should be called when the user selects any asset from the search results. + * + * @property searchRepository local search history storage + */ +class SaveSearchQueryUseCase( + private val searchRepository: SearchRepository, +) { + + /** + * @param query the search text to persist; blank values are silently ignored + */ + suspend operator fun invoke(query: String) { + if (query.isBlank()) return + searchRepository.saveTextHint(query.trim()) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 4a3ebd13c0..01dadd7270 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -392,6 +392,7 @@ include(":domain:yield-supply") include(":domain:yield-supply:models") include(":domain:news") include(":domain:earn") +include(":domain:search") // endregion Domain modules // region Data modules @@ -430,4 +431,5 @@ include(":data:wallet-manager") include(":data:yield-supply") include(":data:news") include(":data:earn") +include(":data:search") // endregion Data modules \ No newline at end of file From db55e6eb1d325564b707b86680d50c1e3acb6bbb Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Mar 2026 12:41:13 +0500 Subject: [PATCH 15/75] Updated on 2026-08-14 --- .../AccountPortfolioItemUMConverter.kt | 29 ++++---- .../DefaultTangemPayCryptoCurrencyFactory.kt | 22 ++++++ .../pay/TangemPayCryptoCurrencyFactory.kt | 1 + .../selector/PortfolioSelectorModel.kt | 8 +- .../converter/AvailableToAddDataConverter.kt | 13 ++-- .../impl/model/MarketsPortfolioDelegate.kt | 16 ++-- .../destination/model/SendDestinationModel.kt | 73 ++++++++++++------- .../SendRecipientWalletListConverter.kt | 3 +- 8 files changed, 101 insertions(+), 64 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt index 150bc43608..c2403a7266 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt @@ -19,23 +19,20 @@ class AccountPortfolioItemUMConverter( private val isBalanceHidden: Boolean = false, private val isEnabled: Boolean = true, private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None, -) : Converter { +) : Converter { - override fun convert(value: Account): UserWalletItemUM { - return when (value) { - is Account.CryptoPortfolio -> with(value) { - UserWalletItemUM( - id = accountId.value, - name = accountName.toUM().value, - information = getInfo(account = this), - balance = getBalanceInfo(), - isEnabled = isEnabled, - endIcon = endIcon, - onClick = onClick, - imageState = getImageState(account = this), - ) - } - is Account.Payment -> TODO("[REDACTED_JIRA]") + override fun convert(value: Account.CryptoPortfolio): UserWalletItemUM { + return with(value) { + UserWalletItemUM( + id = accountId.value, + name = accountName.toUM().value, + information = getInfo(account = this), + balance = getBalanceInfo(), + isEnabled = isEnabled, + endIcon = endIcon, + onClick = onClick, + imageState = getImageState(account = this), + ) } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt index 6a903986b1..48607769ea 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt @@ -8,6 +8,7 @@ import com.tangem.core.error.UniversalError import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.network.NetworkFactory import com.tangem.data.pay.util.TangemPayErrorConverter +import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory @@ -57,4 +58,25 @@ internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor( errorConverter.convert(exception) } } + + override fun create(userWallet: UserWallet): Either { + return catch { + val network = networkFactory.create( + blockchain = VisaUtilities.visaBlockchain, + extraDerivationPath = null, + userWallet = userWallet, + ) + cryptoCurrencyFactory.createToken( + network = requireNotNull(network), + rawId = CryptoCurrency.RawID(TOKEN_ID), + name = TOKEN_NAME, + symbol = TOKEN_NAME, + contractAddress = TOKEN_CONTRACT_ADDRESS, + decimals = TOKEN_DECIMALS, + ) + }.mapLeft { exception -> + TangemLogger.withTag(TAG).e("Error", exception) + errorConverter.convert(exception) + } + } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt index 5f3223e514..1004e00447 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt @@ -8,4 +8,5 @@ import com.tangem.domain.models.wallet.UserWallet interface TangemPayCryptoCurrencyFactory { fun create(userWallet: UserWallet, chainId: Int): Either + fun create(userWallet: UserWallet): Either } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt index b344d503d8..b70b65e101 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.account.PortfolioFetcher @@ -179,13 +180,10 @@ internal class PortfolioSelectorModel @Inject constructor( } } - portfolio.accountsBalance.accountStatuses.forEach { accountStatus -> + portfolio.accountsBalance.accountStatuses.filterCryptoPortfolio().forEach { accountStatus -> val isEnabledByFeature = isEnabled(wallet, accountStatus) val account = accountStatus.account - val accountBalance = when (accountStatus) { - is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance - is AccountStatus.Payment -> accountStatus.value.totalFiatBalance - } + val accountBalance = accountStatus.tokenList.totalFiatBalance val accountItemUM = AccountPortfolioItemUMConverter( onClick = { selectorController.selectAccount(account.accountId) }, appCurrency = appCurrency, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt index a6b02044fb..946ab12577 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt @@ -8,6 +8,7 @@ import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -28,7 +29,7 @@ internal class AvailableToAddDataConverter @Inject constructor( availableNetworks: Set, marketParams: TokenMarketParams, ): AvailableToAddData { - suspend fun AccountStatus.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount? { + suspend fun AccountStatus.CryptoPortfolio.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount? { val currencies = availableNetworks .mapNotNull { network -> createCryptoCurrency( @@ -64,7 +65,7 @@ internal class AvailableToAddDataConverter @Inject constructor( val (_, balance) = entry val wallet = balance.userWallet val filteredNetworks = wallet.filteredAvailableNetworks(availableNetworks) - val accounts = balance.accountsBalance.accountStatuses + val accounts = balance.accountsBalance.accountStatuses.filterCryptoPortfolio() val availableToAddAccounts: Map = accounts .mapNotNull { accountStatus -> val availableToAddAccount = accountStatus.getAvailableToAddAccount(wallet) ?: return@mapNotNull null @@ -103,17 +104,13 @@ internal class AvailableToAddDataConverter @Inject constructor( userWallet: UserWallet, network: TokenMarketInfo.Network, marketParams: TokenMarketParams, - account: Account, + account: Account.CryptoPortfolio, ): CryptoCurrency? { - val derivationIndex = when (account) { - is Account.CryptoPortfolio -> account.derivationIndex - is Account.Payment -> TODO("[REDACTED_JIRA]") - } return getTokenMarketCryptoCurrency( userWalletId = userWallet.walletId, tokenMarketParams = marketParams, network = network, - accountIndex = derivationIndex, + accountIndex = account.derivationIndex, ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt index 50cf8f9881..e1ed13a89e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt @@ -18,6 +18,7 @@ import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -192,15 +193,12 @@ internal class MarketsPortfolioDelegate @AssistedInject constructor( }.distinctUntilChanged() private fun AccountStatusList.filterByRawID(): List { - fun AccountStatus.filterByRawID(): List = when (this) { - is AccountStatus.CryptoPortfolio -> this.tokenList.flattenCurrencies() - .filter { status -> - val currencyId = status.currency.id.rawCurrencyId ?: return@filter false - getTokenIdIfL2Network(currencyId.value) == currencyRawId.value - } - is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") - } - return accountStatuses.map { accountStatus -> + fun AccountStatus.CryptoPortfolio.filterByRawID(): List = tokenList.flattenCurrencies() + .filter { status -> + val currencyId = status.currency.id.rawCurrencyId ?: return@filter false + getTokenIdIfL2Network(currencyId.value) == currencyRawId.value + } + return accountStatuses.filterCryptoPortfolio().map { accountStatus -> AccountWithAdded( accountStatus = accountStatus, addedCurrency = accountStatus.filterByRawID(), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 73df61850a..6d844d1315 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -13,9 +13,13 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase -import com.tangem.domain.models.account.filterCryptoPortfolio +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.CryptoCurrencyAddress +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isLocked +import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase @@ -65,6 +69,7 @@ internal class SendDestinationModel @Inject constructor( private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val parseQrCodeUseCase: ParseQrCodeUseCase, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val tangemPayCryptoCurrencyFactory: TangemPayCryptoCurrencyFactory, private val analyticsEventHandler: AnalyticsEventHandler, private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier, ) : Model(), SendDestinationClickIntents { @@ -217,8 +222,6 @@ internal class SendDestinationModel @Inject constructor( flow = getWalletsUseCase().conflate(), flow2 = multiAccountStatusListSupplier().conflate(), ) { wallets, accountStatusLists -> - val cryptoCurrencyNetwork = cryptoCurrency.network - coroutineScope { accountStatusLists.mapNotNull { accountStatusList -> val wallet = wallets @@ -227,35 +230,55 @@ internal class SendDestinationModel @Inject constructor( ?: return@mapNotNull null async { - accountStatusList.flattenCurrencies() - .filter { it.currency.network.rawId == cryptoCurrencyNetwork.rawId } - .mapNotNull { cryptoCurrencyStatus -> - val address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value - ?: return@mapNotNull null - - // Find the corresponding account from accountStatuses - val account = accountStatusList.accountStatuses - .filterCryptoPortfolio() - .firstOrNull { accountStatus -> - accountStatus.tokenList - .flattenCurrencies() - .any { it.currency.id == cryptoCurrencyStatus.currency.id } - }?.account - - DestinationWalletUM( - name = wallet.name, - address = address, - cryptoCurrency = cryptoCurrencyStatus.currency, - userWalletId = wallet.walletId, - account = account, - ) + accountStatusList.accountStatuses.flatMap { accountStatus -> + when (accountStatus) { + is AccountStatus.CryptoPortfolio -> accountStatus.getDestinationWalletUM(wallet) + is AccountStatus.Payment -> listOfNotNull(accountStatus.getDestinationWalletUM(wallet)) } + } } }.awaitAll().flatten() } }.flowOn(dispatchers.default) } + private fun AccountStatus.CryptoPortfolio.getDestinationWalletUM(wallet: UserWallet): List { + return this.flattenCurrencies() + .filter { it.currency.network.rawId == cryptoCurrency.network.rawId } + .mapNotNull { cryptoCurrencyStatus -> + val address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: return@mapNotNull null + DestinationWalletUM( + name = wallet.name, + address = address, + cryptoCurrency = cryptoCurrencyStatus.currency, + userWalletId = wallet.walletId, + account = account, + ) + } + } + + private fun AccountStatus.Payment.getDestinationWalletUM(wallet: UserWallet): DestinationWalletUM? { + val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: return null + val address = when (val status = this.value) { + is PaymentAccountStatusValue.Loaded -> status.cryptoBalance.depositAddress + is PaymentAccountStatusValue.Locked -> status.cryptoBalance.depositAddress + else -> return null + } + val currency = tangemPayCryptoCurrencyFactory.create(wallet).getOrNull() ?: return null + return if (contractAddress.equals(currency.contractAddress, true)) { + DestinationWalletUM( + name = wallet.name, + address = address, + cryptoCurrency = currency, + userWalletId = wallet.walletId, + account = account, + ) + } else { + null + } + } + private fun validate(address: String, memo: String?, type: EnterAddressSource? = null) { modelScope.launch { _uiState.update(SendDestinationValidationStartedTransformer) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt index 5b47c7de51..6ac08e8caf 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt @@ -35,10 +35,11 @@ internal class SendRecipientWalletListConverter( return this.filterNotNull() .filter { destinationWallet -> val isCoin = destinationWallet.cryptoCurrency is CryptoCurrency.Coin + val isPaymentAccount = destinationWallet.account is Account.Payment val isNotSameAddress = destinationWallet.address != senderAddress val isNotBlankAddress = destinationWallet.address.isNotBlank() - isNotBlankAddress && isCoin && (isNotSameAddress || isSelfSendAvailable) + isNotBlankAddress && (isCoin || isPaymentAccount) && (isNotSameAddress || isSelfSendAvailable) } .groupBy { item -> item.name } .values.map { wallets -> From 7e22f31f66dd779d668c5e0dbdd7ae1d7799ac13 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Mar 2026 10:42:33 +0000 Subject: [PATCH 16/75] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 5f02e77766..40a6e10dec 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.36-1457" +tangemBlockchainSdk = "develop-1455" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.36-599" +tangemCardSdk = "develop-598" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 8223ca8eb85a8727770950ce5dc09805e6ba069a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Mar 2026 17:50:49 +0700 Subject: [PATCH 17/75] Updated on 2026-08-14 --- .../tangem/datasource/api/express/TangemExpressApi.kt | 2 +- .../model/converter/SwapFromSubtitleConverter.kt | 11 ++++++++--- .../com/tangem/feature/swap/DefaultSwapRepository.kt | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index 4d63fd4b3a..04f464e255 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -70,7 +70,7 @@ interface TangemExpressApi { @Query("refundExtraId") refundExtraId: String?, // for cex only @Query("partnerOperationType") partnerOperationType: String?, // swap/ swap-and-send @Query("toExtraId") toExtraId: String?, // swap-and-send memo - @Query("quoteId") quoteId: String?, // swap-and-send memo + @Query("quoteId") quoteId: String?, // fixed rate quoteId ): ApiResponse @GET("exchange-status") diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapFromSubtitleConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapFromSubtitleConverter.kt index ed5c112fd0..696f92438a 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapFromSubtitleConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapFromSubtitleConverter.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.swap.v2.impl.R +import com.tangem.utils.StringsSigns.DOT import java.math.BigDecimal /** @@ -15,7 +16,7 @@ import java.math.BigDecimal * |--------------------------------|-----------------------------------|-----------------------------------|------------------------| * | Entering (float), any | "Balance: " (empty param) | "{balance}" masked | OffsetEnd(symbol) | * | Viewing (fixed), empty | "Balance: " (empty param) | "{balance}" masked (crypto only) | End | - * | Viewing (fixed), not empty | send_from_title | "{displayStr}" masked | OffsetEnd(symbol) | + * | Viewing (fixed), not empty | "{balance}" masked | "• Send {displayStr}" masked | OffsetEnd(symbol) | */ internal object SwapFromSubtitleConverter { @@ -52,9 +53,13 @@ internal object SwapFromSubtitleConverter { ellipsisLeft = TextEllipsis.End } else -> { - subtitleLeft = resourceReference(R.string.send_from_title) - subtitleRight = combinedReference(stringReference(displayStr)) + subtitleLeft = stringReference(balance) .orMaskWithStars(isBalanceHidden) + subtitleRight = combinedReference( + stringReference("$DOT "), + resourceReference(R.string.common_send), + stringReference(" $displayStr"), + ).orMaskWithStars(isBalanceHidden) ellipsisLeft = TextEllipsis.OffsetEnd(symbol.length) } } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index 968145424e..deff8ff1f2 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -344,7 +344,7 @@ internal class DefaultSwapRepository( appPreferencesStore = appPreferencesStore, ), toExtraId = toExtraId?.ifEmpty { null }, - quoteId = null, + quoteId = null, // TODO add when implementing fixed rate in swap ).getOrThrow() if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) { val txDetails = parseTxDetails(response.txDetailsJson) From 2f7bcb9cbb90cfbc9aeafa76bccbbc679ada3f0d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Mar 2026 16:27:20 +0400 Subject: [PATCH 18/75] Updated on 2026-08-14 --- .../java/com/tangem/tap/common/Handler.kt | 16 --- .../events/ScanFailsDialogAnalytics.kt | 2 +- .../tap/common/entities/ProgressState.kt | 5 - .../tangem/tap/common/extensions/ViewGroup.kt | 19 --- .../common/toggleWidget/ViewStateWidget.kt | 13 --- .../transitions/OnboardingTransitions.kt | 24 ---- .../features/scanfails/ScanFailsComponent.kt | 42 +++++++ .../tap/features/scanfails/ScanFailsModel.kt | 84 ++++++++++++++ .../scanfails/ScanFailsRequesterProxy.kt | 21 ++++ .../features/scanfails/di/ScanFailsModule.kt | 32 +++++ .../scanfails/ui/ScanFailsDialogContent.kt | 109 ++++++++++++++++++ .../tap/features/scanfails/ui/ScanFailsUM.kt | 8 ++ .../com/tangem/common/TangemBlogUrlBuilder.kt | 4 + .../tangem/domain/card/ScanFailsRequester.kt | 12 ++ .../HotAccessCodeRequestFullScreenContent.kt | 13 +-- 15 files changed, 316 insertions(+), 88 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/Handler.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/entities/ProgressState.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/extensions/ViewGroup.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/toggleWidget/ViewStateWidget.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/transitions/OnboardingTransitions.kt create mode 100644 app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsComponent.kt create mode 100644 app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsModel.kt create mode 100644 app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsRequesterProxy.kt create mode 100644 app/src/main/java/com/tangem/tap/features/scanfails/di/ScanFailsModule.kt create mode 100644 app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsDialogContent.kt create mode 100644 app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsUM.kt create mode 100644 domain/card/src/main/kotlin/com/tangem/domain/card/ScanFailsRequester.kt diff --git a/app/src/main/java/com/tangem/tap/common/Handler.kt b/app/src/main/java/com/tangem/tap/common/Handler.kt deleted file mode 100644 index 7842da9fbf..0000000000 --- a/app/src/main/java/com/tangem/tap/common/Handler.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.tap.common - -import android.os.Handler -import android.os.HandlerThread -import android.os.Looper - -private val uiHandler = Handler(Looper.getMainLooper()) -private val backgroundHandler = Handler(HandlerThread("AppMainHandlerThread").apply { start() }.looper) - -fun postUi(ms: Long = 0, func: Runnable) { - if (ms == 0L) uiHandler.post { func.run() } else uiHandler.postDelayed(func, ms) -} - -fun postUiDelayBg(ms: Long, func: Runnable) { - backgroundHandler.postDelayed({ uiHandler.post(func) }, ms) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/ScanFailsDialogAnalytics.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/ScanFailsDialogAnalytics.kt index 392f340405..7a5093628b 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/ScanFailsDialogAnalytics.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/ScanFailsDialogAnalytics.kt @@ -11,7 +11,7 @@ class ScanFailsDialogAnalytics(button: Buttons, source: AnalyticsParam.ScreensSo ), ) { enum class Buttons(val event: String) { - TRY_AGAIN("Try again button"), HOW_TO_SCAN("Button blog"), + TRY_AGAIN("Button try again"), } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/entities/ProgressState.kt b/app/src/main/java/com/tangem/tap/common/entities/ProgressState.kt deleted file mode 100644 index 61adc5cd45..0000000000 --- a/app/src/main/java/com/tangem/tap/common/entities/ProgressState.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.tap.common.entities - -import com.tangem.tap.common.toggleWidget.WidgetState - -enum class ProgressState : WidgetState { Loading, Done, Error } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/ViewGroup.kt b/app/src/main/java/com/tangem/tap/common/extensions/ViewGroup.kt deleted file mode 100644 index 214138e5dc..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/ViewGroup.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.tap.common.extensions - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.transition.AutoTransition -import androidx.transition.Transition -import androidx.transition.TransitionManager - -/** -[REDACTED_AUTHOR] - */ -fun ViewGroup.inflate(viewToInflate: Int, attachToRoot: Boolean = false): View { - return LayoutInflater.from(context).inflate(viewToInflate, this, attachToRoot) -} - -fun ViewGroup.beginDelayedTransition(transition: Transition = AutoTransition()) { - TransitionManager.beginDelayedTransition(this, transition) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/toggleWidget/ViewStateWidget.kt b/app/src/main/java/com/tangem/tap/common/toggleWidget/ViewStateWidget.kt deleted file mode 100644 index 10b6a1f7a6..0000000000 --- a/app/src/main/java/com/tangem/tap/common/toggleWidget/ViewStateWidget.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.tap.common.toggleWidget - -import android.view.View - -/** -[REDACTED_AUTHOR] - */ -interface WidgetState - -interface ViewStateWidget { - val mainView: View - fun changeState(state: WidgetState) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/transitions/OnboardingTransitions.kt b/app/src/main/java/com/tangem/tap/common/transitions/OnboardingTransitions.kt deleted file mode 100644 index e250846a42..0000000000 --- a/app/src/main/java/com/tangem/tap/common/transitions/OnboardingTransitions.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.tap.common.transitions - -import androidx.transition.ChangeBounds -import androidx.transition.ChangeTransform -import androidx.transition.Fade -import androidx.transition.TransitionSet - -class HomeToOnboardingTransition : TransitionSet() { - init { - ordering = ORDERING_TOGETHER - addTransition(Fade()) - addTransition(ChangeTransform()) - addTransition(ChangeBounds()) - } -} - -class InternalNoteLayoutTransition : TransitionSet() { - init { - ordering = ORDERING_TOGETHER - addTransition(ChangeTransform()) - addTransition(ChangeBounds()) - addTransition(Fade()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsComponent.kt b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsComponent.kt new file mode 100644 index 0000000000..ffb2163692 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsComponent.kt @@ -0,0 +1,42 @@ +package com.tangem.tap.features.scanfails + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.card.ScanFailsRequester +import com.tangem.tap.features.scanfails.ui.ScanFailsDialogContent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class ScanFailsComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted params: Unit, +) : AppComponentContext by appComponentContext, ComposableContentComponent, ScanFailsRequester { + + private val model: ScanFailsModel = getOrCreateModel(params) + + override suspend fun show(source: ScanFailsRequester.Source): ScanFailsRequester.Result { + model.show(source) + return model.waitResult() + } + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + + if (state.isShown) { + ScanFailsDialogContent(state = state) + } + } + + @AssistedFactory + interface Factory : ComponentFactory { + override fun create(context: AppComponentContext, params: Unit): ScanFailsComponent + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsModel.kt b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsModel.kt new file mode 100644 index 0000000000..e94e714d65 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsModel.kt @@ -0,0 +1,84 @@ +package com.tangem.tap.features.scanfails + +import com.tangem.common.TangemBlogUrlBuilder +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.card.ScanFailsRequester +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics +import com.tangem.tap.features.scanfails.ui.ScanFailsUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ModelScoped +internal class ScanFailsModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val urlOpener: UrlOpener, +) : Model() { + + private val result = MutableStateFlow(null) + + val uiState: StateFlow + field = MutableStateFlow(ScanFailsUM(onDismiss = ::dismiss)) + + fun show(source: ScanFailsRequester.Source) { + val analyticsSource = source.toAnalyticsSource() + result.value = null + uiState.update { + ScanFailsUM( + isShown = true, + onHowToScan = { onHowToScan(analyticsSource) }, + onRequestSupport = { onRequestSupport(analyticsSource) }, + onDismiss = ::dismiss, + ) + } + } + + suspend fun waitResult(): ScanFailsRequester.Result { + return result.filterNotNull().first().also { result.value = null } + } + + fun dismiss() { + result.value = ScanFailsRequester.Result.Dismissed + uiState.update { it.copy(isShown = false) } + } + + private fun onHowToScan(source: AnalyticsParam.ScreensSources) { + analyticsEventHandler.send( + ScanFailsDialogAnalytics( + button = ScanFailsDialogAnalytics.Buttons.HOW_TO_SCAN, + source = source, + ), + ) + modelScope.launch { + urlOpener.openUrl(TangemBlogUrlBuilder.build(TangemBlogUrlBuilder.Post.HowToScan)) + } + } + + private fun onRequestSupport(source: AnalyticsParam.ScreensSources) { + analyticsEventHandler.send(Basic.ButtonSupport(source)) + modelScope.launch { + sendFeedbackEmailUseCase(type = FeedbackEmailType.ScanningProblem) + } + } + + private fun ScanFailsRequester.Source.toAnalyticsSource(): AnalyticsParam.ScreensSources = when (this) { + ScanFailsRequester.Source.MAIN -> AnalyticsParam.ScreensSources.Main + ScanFailsRequester.Source.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn + ScanFailsRequester.Source.SETTINGS -> AnalyticsParam.ScreensSources.Settings + ScanFailsRequester.Source.INTRO -> AnalyticsParam.ScreensSources.Intro + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsRequesterProxy.kt b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsRequesterProxy.kt new file mode 100644 index 0000000000..806d511714 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsRequesterProxy.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.features.scanfails + +import com.tangem.domain.card.ScanFailsRequester +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeout +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ScanFailsRequesterProxy @Inject constructor() : ScanFailsRequester { + + val componentRequester = MutableStateFlow(null) + + override suspend fun show(source: ScanFailsRequester.Source): ScanFailsRequester.Result { + return withTimeout(timeMillis = 1000) { + componentRequester.filterNotNull().first() + }.show(source) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/di/ScanFailsModule.kt b/app/src/main/java/com/tangem/tap/features/scanfails/di/ScanFailsModule.kt new file mode 100644 index 0000000000..6d637e7594 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/scanfails/di/ScanFailsModule.kt @@ -0,0 +1,32 @@ +package com.tangem.tap.features.scanfails.di + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.decompose.model.Model +import com.tangem.domain.card.ScanFailsRequester +import com.tangem.tap.features.scanfails.ScanFailsComponent +import com.tangem.tap.features.scanfails.ScanFailsModel +import com.tangem.tap.features.scanfails.ScanFailsRequesterProxy +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 ScanFailsModule { + + @Binds + fun bindComponentFactory(impl: ScanFailsComponent.Factory): ComponentFactory + + @Binds + @IntoMap + @ClassKey(ScanFailsModel::class) + fun bindModel(model: ScanFailsModel): Model + + @Binds + @Singleton + fun bindRequester(impl: ScanFailsRequesterProxy): ScanFailsRequester +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsDialogContent.kt b/app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsDialogContent.kt new file mode 100644 index 0000000000..fea911a528 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsDialogContent.kt @@ -0,0 +1,109 @@ +package com.tangem.tap.features.scanfails.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.tangem.core.ui.extensions.LocalUserInteractionTracker +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.trackUserInteraction +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.wallet.R + +@Composable +internal fun ScanFailsDialogContent(state: ScanFailsUM) { + val userInteractionTracker = LocalUserInteractionTracker.current + + Dialog( + onDismissRequest = state.onDismiss, + properties = DialogProperties(dismissOnClickOutside = true), + ) { + Column( + modifier = Modifier + .trackUserInteraction(userInteractionTracker) + .background( + color = TangemTheme.colors.background.primary, + shape = RoundedCornerShape(16.dp), + ) + .padding(vertical = 16.dp, horizontal = 38.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + Text( + text = stringResourceSafe(R.string.common_warning), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h3, + textAlign = TextAlign.Center, + ) + + Text( + text = stringResourceSafe(R.string.alert_troubleshooting_scan_card_title), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + textAlign = TextAlign.Center, + ) + + DialogTextButton( + text = stringResourceSafe(R.string.alert_button_how_to_scan), + onClick = state.onHowToScan, + color = TangemTheme.colors.text.accent, + ) + + DialogTextButton( + text = stringResourceSafe(R.string.alert_button_request_support), + onClick = state.onRequestSupport, + color = TangemTheme.colors.text.accent, + ) + + DialogTextButton( + text = stringResourceSafe(R.string.common_cancel), + onClick = state.onDismiss, + color = TangemTheme.colors.text.warning, + ) + } + } +} + +@Composable +private fun DialogTextButton(text: String, onClick: () -> Unit, color: Color) { + Text( + text = text.uppercase(), + color = color, + style = TangemTheme.typography.button, + textAlign = TextAlign.Center, + modifier = Modifier.clickable(onClick = onClick), + ) +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview(showBackground = true, widthDp = 360, fontScale = 2f) +@Composable +private fun ScanFailsDialogContentPreview() { + TangemThemePreview { + ScanFailsDialogContent( + state = ScanFailsUM( + isShown = true, + onHowToScan = {}, + onRequestSupport = {}, + onDismiss = {}, + ), + ) + } +} +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsUM.kt b/app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsUM.kt new file mode 100644 index 0000000000..6e6ebebb8f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsUM.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.features.scanfails.ui + +internal data class ScanFailsUM( + val isShown: Boolean = false, + val onHowToScan: () -> Unit = {}, + val onRequestSupport: () -> Unit = {}, + val onDismiss: () -> Unit = {}, +) \ No newline at end of file diff --git a/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt index 1ba2efe988..7f1d0b8d98 100644 --- a/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt +++ b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt @@ -35,5 +35,9 @@ object TangemBlogUrlBuilder { data object WhatIsTransactionFee : Post { override val path: String = "what-is-a-transaction-fee-and-why-do-we-need-it" } + + data object HowToScan : Post { + override val path: String = "scan-tangem-card" + } } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanFailsRequester.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanFailsRequester.kt new file mode 100644 index 0000000000..19fbb925c4 --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanFailsRequester.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.card + +interface ScanFailsRequester { + + suspend fun show(source: Source): Result + + enum class Source { MAIN, SIGN_IN, SETTINGS, INTRO } + + sealed class Result { + data object Dismissed : Result() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt index bd9c1ce7d9..26e9825ea2 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt @@ -1,11 +1,7 @@ package com.tangem.features.hotwallet.accesscoderequest.ui -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.* import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Button @@ -26,9 +22,7 @@ import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.fields.PinTextColor import com.tangem.core.ui.components.fields.PinTextField -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.TangemTheme @@ -42,12 +36,11 @@ internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM val userInteractionTracker = LocalUserInteractionTracker.current Box( - modifier = Modifier + modifier = modifier .fillMaxSize() .trackUserInteraction(userInteractionTracker), ) { AnimatedVisibility( - modifier = modifier, visible = state.isShown, enter = fadeIn(), exit = fadeOut(), From d050fa265441845f25e0f5b39a3855c5d131c676 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Mar 2026 17:27:32 +0500 Subject: [PATCH 19/75] Updated on 2026-08-14 --- .../tangem/screens/MainScreenPageObject.kt | 18 --- .../kotlin/com/tangem/tests/WarningTest.kt | 39 ----- .../tap/di/domain/WalletsDomainModule.kt | 6 - .../data/wallets/DefaultWalletsRepository.kt | 148 ------------------ .../data/wallets/di/WalletsDataModule.kt | 2 - .../wallets/DefaultWalletsRepositoryTest.kt | 1 - .../models/SeedPhraseNotificationsStatus.kt | 8 - .../wallets/repository/WalletsRepository.kt | 13 -- .../usecase/SeedPhraseNotificationUseCase.kt | 35 ----- .../intents/WalletWarningsClickIntents.kt | 71 --------- .../analytics/WalletScreenAnalyticsEvent.kt | 12 -- .../utils/WalletWarningsAnalyticsSender.kt | 4 - .../utils/WalletWarningsSingleEventSender.kt | 20 +-- .../domain/GetMultiWalletWarningsFactory.kt | 55 +------ .../domain/GetWalletNotificationsFactory.kt | 44 +----- .../wallet/state/model/WalletAlertUM.kt | 18 --- .../wallet/state/model/WalletNotification.kt | 28 ---- .../state/model/WalletNotificationUM.kt | 58 ------- 18 files changed, 12 insertions(+), 568 deletions(-) delete mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/models/SeedPhraseNotificationsStatus.kt delete mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SeedPhraseNotificationUseCase.kt diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt index 0e2ea89335..56036ef56a 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt @@ -154,24 +154,6 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) useUnmergedTree = true } - val seedPhraseNotificationIcon: KNode = child { - hasAnySibling(withText(getResourceString(R.string.warning_seedphrase_issue_title))) - hasTestTag(NotificationTestTags.ICON) - useUnmergedTree = true - } - - val seedPhraseNotificationTitle: KNode = child { - hasTestTag(NotificationTestTags.TITLE) - hasText(getResourceString(R.string.warning_seedphrase_issue_title)) - useUnmergedTree = true - } - - val seedPhraseNotificationMessage: KNode = child { - hasTestTag(NotificationTestTags.MESSAGE) - hasText(getResourceString(R.string.warning_seedphrase_issue_message)) - useUnmergedTree = true - } - val totalBalanceContainer: KNode = child { hasTestTag(MainScreenTestTags.WALLET_LIST_ITEM) } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt index 8bbaf0ed91..51bc278976 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt @@ -55,43 +55,4 @@ class WarningTest : BaseTestCase() { } } } - - @AllureId("227") - @DisplayName("Seed notify: check warning for wallet with seed phrase") - @Test - fun checkWarningForWalletWithSeedPhraseTest() { - val scenarioName = "seedphrase_notification" - val scenarioState = "Notified" - setupHooks( - additionalBeforeSection = { - step("Setup WireMock scenario '$scenarioName' for '$scenarioState' state") { - setWireMockScenarioState(scenarioName, scenarioState) - } - }, - additionalAfterSection = { - step("Reset WireMock scenario '$scenarioName' state") { - resetWireMockScenarioState(scenarioName) - } - } - ).run { - step("Open 'Main' screen") { - openMainScreen(mockContent = Wallet2WithSeedPhraseMockContent) - } - step("Assert 'Seed phrase' notification icon is displayed") { - onMainScreen { seedPhraseNotificationIcon.assertIsDisplayed() } - } - step("Assert 'Seed phrase' notification title is displayed") { - onMainScreen { seedPhraseNotificationTitle.assertIsDisplayed() } - } - step("Assert 'Seed phrase' notification message is displayed") { - onMainScreen { seedPhraseNotificationMessage.assertIsDisplayed() } - } - step("Assert notification 'Yes' button is displayed") { - onMainScreen { notificationYesButton.assertIsDisplayed() } - } - step("Assert notification 'No' button is displayed") { - onMainScreen { notificationNoButton.assertIsDisplayed() } - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index c63293cda2..04f3106b33 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -222,12 +222,6 @@ internal object WalletsDomainModule { ) } - @Provides - @Singleton - fun providesSeedPhraseNotificationUseCase(walletsRepository: WalletsRepository): SeedPhraseNotificationUseCase { - return SeedPhraseNotificationUseCase(walletsRepository = walletsRepository) - } - @Provides @Singleton fun provideGetCardImageUseCase(cardArtworksProvider: CardArtworksProvider): GetCardImageUseCase { diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index e2fac0c166..5a5278632e 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -12,11 +12,8 @@ import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.common.response.isNetworkError import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.* -import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status -import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.PreferencesKeys.SEED_FIRST_NOTIFICATION_SHOW_TIME import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.getSyncOrNull @@ -26,27 +23,20 @@ import com.tangem.domain.common.wallets.getSyncOrNull import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.models.UserWalletRemoteInfo import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.utils.WEEK_MILLIS import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runCatching import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -typealias SeedPhraseNotificationsStatuses = Map - @Suppress("TooManyFunctions", "LargeClass", "LongParameterList") internal class DefaultWalletsRepository( private val appPreferencesStore: AppPreferencesStore, private val tangemTechApi: TangemTechApi, private val userWalletsListRepository: UserWalletsListRepository, - private val seedPhraseNotificationVisibilityStore: RuntimeStateStore, private val dispatchers: CoroutineDispatcherProvider, private val walletServerBinder: WalletServerBinder, private val moshi: com.squareup.moshi.Moshi, @@ -128,148 +118,10 @@ internal class DefaultWalletsRepository( } } - override fun seedPhraseNotificationStatus(userWalletId: UserWalletId): Flow { - return channelFlow { - launch { - seedPhraseNotificationVisibilityStore.get() - .map { map -> - map.getOrDefault( - key = userWalletId, - defaultValue = SeedPhraseNotificationsStatus.NOT_NEEDED, - ) - } - .collectLatest(::send) - } - - fetchSeedPhraseNotificationStatus(userWalletId) - } - } - - private suspend fun fetchSeedPhraseNotificationStatus(userWalletId: UserWalletId) { - val userWallet = userWalletsListRepository.getSyncOrNull(id = userWalletId) - - if (userWallet != null && userWallet !is UserWallet.Cold) { - updateNotificationVisibility(id = userWalletId, value = SeedPhraseNotificationsStatus.NOT_NEEDED) - return - } - - val status = if (userWallet?.isImported == false) { - Status.NOT_NEEDED - } else { - runCatching(dispatchers.io) { - tangemTechApi.getSeedPhraseNotificationStatus(walletId = userWalletId.stringValue).getOrThrow() - }.fold( - onSuccess = { it.status }, - onFailure = { throwable -> - if (throwable is HttpException && throwable.code == HttpException.Code.NOT_FOUND) { - Status.NOTIFIED - } else { - Status.NOT_NEEDED - } - }, - ) - } - - when { - status == Status.NOTIFIED -> updateNotificationVisibility( - id = userWalletId, - value = SeedPhraseNotificationsStatus.SHOW_FIRST, - ) - status == Status.CONFIRMED && checkNeedFetchSecondNotification() -> - fetchSeedPhraseSecondNotificationStatus(userWalletId) - else -> updateNotificationVisibility(id = userWalletId, value = SeedPhraseNotificationsStatus.NOT_NEEDED) - } - } - - private suspend fun fetchSeedPhraseSecondNotificationStatus(userWalletId: UserWalletId) { - val status = runCatching(dispatchers.io) { - tangemTechApi.getSeedPhraseSecondNotificationStatus(walletId = userWalletId.stringValue).getOrThrow() - }.fold( - onSuccess = { it.status }, - onFailure = { Status.NOT_NEEDED }, - ) - - val showStatus = when (status) { - Status.CONFIRMED -> SeedPhraseNotificationsStatus.SHOW_SECOND - else -> SeedPhraseNotificationsStatus.NOT_NEEDED - } - - updateNotificationVisibility(id = userWalletId, value = showStatus) - } - - private suspend fun checkNeedFetchSecondNotification(): Boolean { - val firstNotificationTime = - appPreferencesStore.getSyncOrDefault(SEED_FIRST_NOTIFICATION_SHOW_TIME, default = 0L) - return System.currentTimeMillis() - firstNotificationTime > WEEK_MILLIS - } - - override suspend fun notifiedSeedPhraseNotification(userWalletId: UserWalletId) { - runCatching(dispatchers.io) { - tangemTechApi.updateSeedPhraseNotificationStatus( - walletId = userWalletId.stringValue, - body = SeedPhraseNotificationDTO(status = Status.NOTIFIED), - ).getOrThrow() - } - } - - override suspend fun confirmSeedPhraseNotification(userWalletId: UserWalletId) { - runCatching(dispatchers.io) { - tangemTechApi.updateSeedPhraseNotificationStatus( - walletId = userWalletId.stringValue, - body = SeedPhraseNotificationDTO(status = Status.CONFIRMED), - ).getOrThrow() - appPreferencesStore.store(key = SEED_FIRST_NOTIFICATION_SHOW_TIME, value = System.currentTimeMillis()) - } - - updateNotificationVisibility(id = userWalletId, value = SeedPhraseNotificationsStatus.NOT_NEEDED) - } - - override suspend fun declineSeedPhraseNotification(userWalletId: UserWalletId) { - runCatching(dispatchers.io) { - tangemTechApi.updateSeedPhraseNotificationStatus( - walletId = userWalletId.stringValue, - body = SeedPhraseNotificationDTO(status = Status.DECLINED), - ).getOrThrow() - appPreferencesStore.store(key = SEED_FIRST_NOTIFICATION_SHOW_TIME, value = System.currentTimeMillis()) - } - - updateNotificationVisibility(id = userWalletId, value = SeedPhraseNotificationsStatus.NOT_NEEDED) - } - override suspend fun createWallet(userWalletId: UserWalletId) { walletServerBinder.bind(userWalletId) } - override suspend fun rejectSeedPhraseSecondNotification(userWalletId: UserWalletId) { - runCatching(dispatchers.io) { - tangemTechApi.updateSeedPhraseSecondNotificationStatus( - walletId = userWalletId.stringValue, - body = SeedPhraseNotificationDTO(status = Status.REJECTED), - ).getOrThrow() - } - - updateNotificationVisibility(id = userWalletId, value = SeedPhraseNotificationsStatus.NOT_NEEDED) - } - - override suspend fun acceptSeedPhraseSecondNotification(userWalletId: UserWalletId) { - runCatching(dispatchers.io) { - tangemTechApi.updateSeedPhraseSecondNotificationStatus( - walletId = userWalletId.stringValue, - body = SeedPhraseNotificationDTO(status = Status.ACCEPTED), - ).getOrThrow() - } - - updateNotificationVisibility(id = userWalletId, value = SeedPhraseNotificationsStatus.NOT_NEEDED) - } - - private suspend fun updateNotificationVisibility(id: UserWalletId, value: SeedPhraseNotificationsStatus) { - return seedPhraseNotificationVisibilityStore.update { map -> - map.toMutableMap().apply { - this[id] = value - } - } - } - override fun nftEnabledStatus(userWalletId: UserWalletId): Flow = appPreferencesStore .getObjectMap(PreferencesKeys.WALLETS_NFT_ENABLED_STATES_KEY) .map { it[userWalletId.stringValue] == true } diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt index c7561aab8b..1d57f16125 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -12,7 +12,6 @@ import com.tangem.data.wallets.hot.DefaultHotWalletAccessCodeAttemptsRepository import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.appsflyer.AppsFlyerStore -import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository @@ -48,7 +47,6 @@ internal object WalletsDataModule { appPreferencesStore = appPreferencesStore, tangemTechApi = tangemTechApi, userWalletsListRepository = userWalletsListRepository, - seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()), dispatchers = dispatchers, walletServerBinder = walletServerBinder, moshi = moshi, diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt index 639b115e55..51046c6622 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -45,7 +45,6 @@ class DefaultWalletsRepositoryTest { appPreferencesStore = appPreferenceStore, tangemTechApi = tangemTechApi, userWalletsListRepository = mockk(), - seedPhraseNotificationVisibilityStore = mockk(), dispatchers = TestingCoroutineDispatcherProvider(), walletServerBinder = walletServerBinder, moshi = mockk(), diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SeedPhraseNotificationsStatus.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SeedPhraseNotificationsStatus.kt deleted file mode 100644 index 2c9180e6a6..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SeedPhraseNotificationsStatus.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.domain.wallets.models - -enum class SeedPhraseNotificationsStatus { - - SHOW_FIRST, - SHOW_SECOND, - NOT_NEEDED, -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 06baab2842..2b25388b1e 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -3,7 +3,6 @@ package com.tangem.domain.wallets.repository import arrow.core.Either import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.models.UserWalletRemoteInfo import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError import kotlinx.coroutines.flow.Flow @@ -23,18 +22,6 @@ interface WalletsRepository { suspend fun setHasWalletsWithRing(userWalletId: UserWalletId) - fun seedPhraseNotificationStatus(userWalletId: UserWalletId): Flow - - suspend fun notifiedSeedPhraseNotification(userWalletId: UserWalletId) - - suspend fun confirmSeedPhraseNotification(userWalletId: UserWalletId) - - suspend fun declineSeedPhraseNotification(userWalletId: UserWalletId) - - suspend fun rejectSeedPhraseSecondNotification(userWalletId: UserWalletId) - - suspend fun acceptSeedPhraseSecondNotification(userWalletId: UserWalletId) - suspend fun createWallet(userWalletId: UserWalletId) fun nftEnabledStatus(userWalletId: UserWalletId): Flow diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SeedPhraseNotificationUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SeedPhraseNotificationUseCase.kt deleted file mode 100644 index 12326b4ac6..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SeedPhraseNotificationUseCase.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.domain.wallets.usecase - -import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.repository.WalletsRepository -import kotlinx.coroutines.flow.Flow - -class SeedPhraseNotificationUseCase( - private val walletsRepository: WalletsRepository, -) { - - operator fun invoke(userWalletId: UserWalletId): Flow { - return walletsRepository.seedPhraseNotificationStatus(userWalletId) - } - - suspend fun notified(userWalletId: UserWalletId) { - walletsRepository.notifiedSeedPhraseNotification(userWalletId) - } - - suspend fun confirm(userWalletId: UserWalletId) { - walletsRepository.confirmSeedPhraseNotification(userWalletId) - } - - suspend fun decline(userWalletId: UserWalletId) { - walletsRepository.declineSeedPhraseNotification(userWalletId) - } - - suspend fun acceptSecond(userWalletId: UserWalletId) { - walletsRepository.acceptSeedPhraseSecondNotification(userWalletId) - } - - suspend fun rejectSecond(userWalletId: UserWalletId) { - walletsRepository.rejectSeedPhraseSecondNotification(userWalletId) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index 5d86ed9512..ee38dd19e0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -2,7 +2,6 @@ package com.tangem.feature.wallet.child.wallet.model.intents import arrow.core.getOrElse import com.tangem.utils.logging.TangemLogger -import com.tangem.common.TangemBlogUrlBuilder import com.tangem.common.routing.AppRoute.* import com.tangem.common.routing.AppRouter import com.tangem.common.ui.notifications.NotificationId @@ -45,7 +44,6 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnaly import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents @@ -85,14 +83,6 @@ internal interface WalletWarningsClickIntents { fun onNoteMigrationButtonClick(url: String) - fun onSeedPhraseNotificationConfirm() - - fun onSeedPhraseNotificationDecline() - - fun onSeedPhraseSecondNotificationAccept() - - fun onSeedPhraseSecondNotificationReject() - fun onAllowPermissions() fun onDenyPermissions() @@ -122,7 +112,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase, private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, - private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase, private val urlOpener: UrlOpener, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, @@ -360,66 +349,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } - override fun onSeedPhraseNotificationConfirm() { - val userWallet = getSelectedUserWallet() ?: return - - analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonYes()) - - uiMessageSender.send( - WalletAlertUM.seedPhraseConfirm { - modelScope.launch { - seedPhraseNotificationUseCase.confirm(userWalletId = userWallet.walletId) - - urlOpener.openUrl( - url = TangemBlogUrlBuilder.build(post = TangemBlogUrlBuilder.Post.SeedNotify), - ) - } - }, - ) - } - - override fun onSeedPhraseNotificationDecline() { - val userWallet = getSelectedUserWallet() ?: return - - analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonNo()) - - uiMessageSender.send( - WalletAlertUM.seedPhraseDismiss { - modelScope.launch { - seedPhraseNotificationUseCase.decline(userWalletId = userWallet.walletId) - } - }, - ) - } - - override fun onSeedPhraseSecondNotificationAccept() { - val userWallet = getSelectedUserWallet() ?: return - - analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonUsed()) - - uiMessageSender.send( - WalletAlertUM.seedPhraseConfirm { - modelScope.launch { - seedPhraseNotificationUseCase.acceptSecond(userWalletId = userWallet.walletId) - - urlOpener.openUrl( - url = TangemBlogUrlBuilder.build(post = TangemBlogUrlBuilder.Post.SeedNotifySecond), - ) - } - }, - ) - } - - override fun onSeedPhraseSecondNotificationReject() { - val userWallet = getSelectedUserWallet() ?: return - - analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonDeclined()) - - modelScope.launch { - seedPhraseNotificationUseCase.rejectSecond(userWalletId = userWallet.walletId) - } - } - override fun onFinishWalletActivationClick(isBackupExists: Boolean) { analyticsEventHandler.send(MainScreen.ButtonFinalizeActivation()) val userWalletId = stateHolder.getSelectedWalletId() 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 71ae06f02a..5040a3728d 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 @@ -155,18 +155,6 @@ sealed class WalletScreenAnalyticsEvent { class DeleteWalletTapped : MainScreen(event = "Button - Delete Wallet Tapped") - class NoticeSeedPhraseSupport : MainScreen(event = "Notice - Seed Phrase Support") - - class NoticeSeedPhraseSupportSecond : MainScreen(event = "Notice - Seed Phrase Support2") - - class NoticeSeedPhraseSupportButtonNo : MainScreen(event = "Button - Support No") - - class NoticeSeedPhraseSupportButtonYes : MainScreen(event = "Button - Support Yes") - - class NoticeSeedPhraseSupportButtonUsed : MainScreen(event = "Button - Support Used") - - class NoticeSeedPhraseSupportButtonDeclined : MainScreen(event = "Button - Support Declined") - class NoticeUnrecognizedQr : MainScreen( event = "Notice - Unrecognized QR", ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 09f8eba96e..32b3a72da1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -116,8 +116,6 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( balanceState = balanceState, ) } - is WalletNotification.Critical.SeedPhraseNotification -> NoticeSeedPhraseSupport() - is WalletNotification.Critical.SeedPhraseSecondNotification -> NoticeSeedPhraseSupportSecond() is WalletNotification.PushNotifications -> PushBanner() is WalletNotification.Warning.TangemPayRefreshNeeded -> null is WalletNotification.Warning.TangemPayUnreachable -> null @@ -163,8 +161,6 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( balanceState = balanceState, ) } - is WalletNotificationUM.SeedPhraseNotification -> NoticeSeedPhraseSupport() - is WalletNotificationUM.SeedPhraseSecondNotification -> NoticeSeedPhraseSupportSecond() is WalletNotificationUM.PushNotifications -> PushBanner() is WalletNotificationUM.UnlockWallets, is WalletNotificationUM.NoAccount, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt index ee14b94350..2a7b1a0a47 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt @@ -11,7 +11,6 @@ import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification @@ -24,7 +23,6 @@ import javax.inject.Inject @ModelScoped internal class WalletWarningsSingleEventSender @Inject constructor( - private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase, private val screenLifecycleProvider: ScreenLifecycleProvider, private val uiMessageSender: UiMessageSender, private val getUserWalletUseCase: GetUserWalletUseCase, @@ -32,11 +30,7 @@ internal class WalletWarningsSingleEventSender @Inject constructor( ) { private val isActivationBottomSheetShown: ConcurrentHashMap = ConcurrentHashMap() - suspend fun send( - userWalletId: UserWalletId, - displayedUiState: WalletState?, - newWarnings: List, - ) { + fun send(userWalletId: UserWalletId, displayedUiState: WalletState?, newWarnings: List) { if (screenLifecycleProvider.isBackgroundState.value) return if (newWarnings.isEmpty()) return if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return @@ -51,9 +45,6 @@ internal class WalletWarningsSingleEventSender @Inject constructor( events.forEach { event -> when (event) { - is WalletNotification.Critical.SeedPhraseNotification -> { - seedPhraseNotificationUseCase.notified(userWalletId = userWalletId) - } is WalletNotification.FinishWalletActivation -> { // We check that map contains the first seen wallet (will return null instead false/true otherwise) // and for this wallet we haven't shown the activation bs yet (check that returns false, not true) @@ -69,11 +60,7 @@ internal class WalletWarningsSingleEventSender @Inject constructor( } } - suspend fun send( - userWalletId: UserWalletId, - displayedWalletUM: WalletUM?, - newNotifications: List, - ) { + fun send(userWalletId: UserWalletId, displayedWalletUM: WalletUM?, newNotifications: List) { if (screenLifecycleProvider.isBackgroundState.value) return if (newNotifications.isEmpty()) return if (displayedWalletUM == null || displayedWalletUM.pullToRefreshConfig.isRefreshing) return @@ -89,9 +76,6 @@ internal class WalletWarningsSingleEventSender @Inject constructor( events.forEach { event -> when (event) { - is WalletNotificationUM.SeedPhraseNotification -> { - seedPhraseNotificationUseCase.notified(userWalletId = userWalletId) - } is WalletNotificationUM.FinishWalletActivation -> { // We check that map contains the first seen wallet (will return null instead false/true otherwise) // and for this wallet we haven't shown the activation bs yet (check that returns false, not true) 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 e81d00263d..fd7ada0172 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 @@ -27,9 +27,7 @@ import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.promo.ShouldShowPromoWalletUseCase import com.tangem.domain.promo.models.PromoId import com.tangem.domain.settings.IsReadyToShowRateAppUseCase -import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase -import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.impl.R @@ -56,7 +54,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, private val backupValidator: BackupValidator, - private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase, private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase, private val notificationsRepository: NotificationsRepository, private val accountDependencies: AccountDependencies, @@ -76,7 +73,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( accountStatusListFlow, isReadyToShowRateAppUseCase().distinctUntilChanged(), isNeedToBackupUseCase(userWallet.walletId).distinctUntilChanged(), - seedPhraseNotificationUseCase(userWalletId = userWallet.walletId).distinctUntilChanged(), shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.OnePlusOne) .distinctUntilChanged(), notificationsRepository.getShouldShowNotification(NotificationId.EnablePushesReminderNotification.key) @@ -93,13 +89,12 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val accountStatusList = array[0] as AccountStatusList val isReadyToShowRating = array[1] as Boolean val isNeedToBackup = array[2] as Boolean - val seedPhraseIssueStatus = array[3] as SeedPhraseNotificationsStatus - val shouldShowOnePlusOnePromo = array[4] as Boolean - val shouldShowEnablePushesReminderNotification = array[5] as Boolean - val shouldAccessCodeSkipped = array[6] as Boolean - val shouldShowYieldPromo = array[7] as Boolean - val shouldShowUpgradeBanner = array[8] as Boolean - val closureTimestamp = array[9] as? Long + val shouldShowOnePlusOnePromo = array[3] as Boolean + val shouldShowEnablePushesReminderNotification = array[4] as Boolean + val shouldAccessCodeSkipped = array[5] as Boolean + val shouldShowYieldPromo = array[6] as Boolean + val shouldShowUpgradeBanner = array[7] as Boolean + val closureTimestamp = array[8] as? Long val flattenCurrencies = accountStatusList.flattenCurrencies() val paymentAccountStatus = accountStatusList.accountStatuses @@ -109,7 +104,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( buildList { addUsedOutdatedDataNotification(accountStatusList.totalFiatBalance) - addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) + addCriticalNotifications(userWallet, clickIntents) addUpgradeHotWalletPromoNotification( userWallet = userWallet, @@ -213,15 +208,12 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private fun MutableList.addCriticalNotifications( userWallet: UserWallet, - seedPhraseIssueStatus: SeedPhraseNotificationsStatus, clickIntents: WalletClickIntents, ) { if (userWallet !is UserWallet.Cold) { return } - addSeedNotificationIfNeeded(userWallet, seedPhraseIssueStatus, clickIntents) - val cardTypesResolver = userWallet.scanResponse.cardTypesResolver addIf( element = WalletNotification.Critical.BackupError { clickIntents.onBackupErrorClick() }, @@ -246,39 +238,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( } } - private fun MutableList.addSeedNotificationIfNeeded( - userWallet: UserWallet.Cold, - seedPhraseIssueStatus: SeedPhraseNotificationsStatus, - clickIntents: WalletClickIntents, - ) { - val isNotificationAvailable = with(userWallet) { - val isDemo = isDemoCardUseCase(cardId = userWallet.cardId) - val isWalletWithSeedPhrase = scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported - - !isDemo && isWalletWithSeedPhrase - } - - when (seedPhraseIssueStatus) { - SeedPhraseNotificationsStatus.SHOW_FIRST -> addIf( - element = WalletNotification.Critical.SeedPhraseNotification( - onDeclineClick = clickIntents::onSeedPhraseNotificationDecline, - onConfirmClick = clickIntents::onSeedPhraseNotificationConfirm, - ), - condition = isNotificationAvailable, - ) - SeedPhraseNotificationsStatus.SHOW_SECOND -> addIf( - element = WalletNotification.Critical.SeedPhraseSecondNotification( - onDeclineClick = clickIntents::onSeedPhraseSecondNotificationReject, - onConfirmClick = clickIntents::onSeedPhraseSecondNotificationAccept, - ), - condition = isNotificationAvailable, - ) - SeedPhraseNotificationsStatus.NOT_NEEDED -> { - // do nothing - } - } - } - private fun MutableList.addInformationalNotifications( userWallet: UserWallet, cardTypesResolver: CardTypesResolver?, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt index 8735cf42f4..51f3323547 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt @@ -14,9 +14,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase -import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM @@ -40,7 +38,6 @@ internal class GetWalletNotificationsFactory @Inject constructor( private val isDemoCardUseCase: IsDemoCardUseCase, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, private val backupValidator: BackupValidator, - private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase, private val accountDependencies: AccountDependencies, private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, @@ -54,16 +51,15 @@ internal class GetWalletNotificationsFactory @Inject constructor( return combine( flow = accountStatusListFlow, flow2 = isNeedToBackupUseCase(userWallet.walletId).distinctUntilChanged(), - flow3 = seedPhraseNotificationUseCase(userWalletId = userWallet.walletId).distinctUntilChanged(), - flow4 = getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(), - ) { accountList, isNeedToBackup, seedPhraseIssueStatus, shouldAccessCodeSkipped -> + flow3 = getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(), + ) { accountList, isNeedToBackup, shouldAccessCodeSkipped -> val totalFiatBalance = accountList.totalFiatBalance val flattenCurrencies = accountList.flattenCurrencies() buildList { addUsedOutdatedDataNotification(totalFiatBalance) - addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) + addCriticalNotifications(userWallet, clickIntents) addFinishWalletActivationNotification( userWallet = userWallet, @@ -99,15 +95,12 @@ internal class GetWalletNotificationsFactory @Inject constructor( private fun MutableList.addCriticalNotifications( userWallet: UserWallet, - seedPhraseIssueStatus: SeedPhraseNotificationsStatus, clickIntents: WalletClickIntents, ) { if (userWallet !is UserWallet.Cold) { return } - addSeedNotificationIfNeeded(userWallet, seedPhraseIssueStatus, clickIntents) - val cardTypesResolver = userWallet.scanResponse.cardTypesResolver addIf( element = WalletNotificationUM.BackupError { clickIntents.onSupportClick() }, @@ -279,37 +272,6 @@ internal class GetWalletNotificationsFactory @Inject constructor( ) } - private fun MutableList.addSeedNotificationIfNeeded( - userWallet: UserWallet.Cold, - seedPhraseIssueStatus: SeedPhraseNotificationsStatus, - clickIntents: WalletClickIntents, - ) { - val isNotificationAvailable = with(userWallet) { - val isDemo = isDemoCardUseCase(cardId = userWallet.cardId) - val isWalletWithSeedPhrase = scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported - - !isDemo && isWalletWithSeedPhrase - } - - when (seedPhraseIssueStatus) { - SeedPhraseNotificationsStatus.SHOW_FIRST -> addIf( - element = WalletNotificationUM.SeedPhraseNotification( - onDeclineClick = clickIntents::onSeedPhraseNotificationDecline, - onConfirmClick = clickIntents::onSeedPhraseNotificationConfirm, - ), - condition = isNotificationAvailable, - ) - SeedPhraseNotificationsStatus.SHOW_SECOND -> addIf( - element = WalletNotificationUM.SeedPhraseSecondNotification( - onDeclineClick = clickIntents::onSeedPhraseSecondNotificationReject, - onConfirmClick = clickIntents::onSeedPhraseSecondNotificationAccept, - ), - condition = isNotificationAvailable, - ) - SeedPhraseNotificationsStatus.NOT_NEEDED -> Unit - } - } - private suspend fun hasSignedHashes( selectedWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus?, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertUM.kt index 665fcfdafd..c63587fb17 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertUM.kt @@ -9,24 +9,6 @@ import com.tangem.feature.wallet.impl.R internal object WalletAlertUM { - fun seedPhraseConfirm(onClick: () -> Unit): DialogMessage { - return DialogMessage( - message = resourceReference(R.string.warning_seedphrase_issue_answer_yes), - firstActionBuilder = { - okAction(onClick = onClick) - }, - ) - } - - fun seedPhraseDismiss(onClick: () -> Unit): DialogMessage { - return DialogMessage( - message = resourceReference(R.string.warning_seedphrase_issue_answer_no), - firstActionBuilder = { - okAction(onClick = onClick) - }, - ) - } - fun unableHideToken(cryptoCurrency: CryptoCurrency): DialogMessage { return DialogMessage( title = resourceReference( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 77f9417e93..c960e5ba67 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -55,34 +55,6 @@ sealed class WalletNotification(val config: NotificationConfig) { onClick = onSupportClick, ), ) - - data class SeedPhraseNotification( - val onDeclineClick: () -> Unit, - val onConfirmClick: () -> Unit, - ) : Critical( - title = resourceReference(R.string.warning_seedphrase_issue_title), - subtitle = resourceReference(R.string.warning_seedphrase_issue_message), - buttonsState = NotificationConfig.ButtonsState.SecondaryPairButtonsConfig( - leftText = resourceReference(R.string.common_no), - onLeftClick = onDeclineClick, - rightText = resourceReference(R.string.common_yes), - onRightClick = onConfirmClick, - ), - ) - - data class SeedPhraseSecondNotification( - val onDeclineClick: () -> Unit, - val onConfirmClick: () -> Unit, - ) : Critical( - title = resourceReference(R.string.warning_seedphrase_action_required_title), - subtitle = resourceReference(R.string.warning_seedphrase_contacted_support), - buttonsState = NotificationConfig.ButtonsState.SecondaryPairButtonsConfig( - leftText = resourceReference(R.string.seed_warning_no), - onLeftClick = onDeclineClick, - rightText = resourceReference(R.string.seed_warning_yes), - onRightClick = onConfirmClick, - ), - ) } sealed class Warning( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt index f0c7a36808..f3644ec928 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt @@ -143,64 +143,6 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t type = WalletNotificationType.Critical, ) - data class SeedPhraseNotification( - val onDeclineClick: () -> Unit, - val onConfirmClick: () -> Unit, - ) : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "SeedPhraseIssueNotification", - title = resourceReference(id = R.string.warning_seedphrase_issue_title), - subtitle = resourceReference(id = R.string.warning_seedphrase_issue_message), - messageEffect = TangemMessageEffect.Warning, - buttonsUM = persistentListOf( - TangemMessageButtonUM( - text = resourceReference(id = R.string.common_no), - type = TangemButtonType.PrimaryInverse, - onClick = onDeclineClick, - ), - TangemMessageButtonUM( - text = resourceReference(id = R.string.common_yes), - type = TangemButtonType.PrimaryInverse, - onClick = onConfirmClick, - ), - ), - iconUM = TangemIconUM.Icon( - iconRes = R.drawable.ic_attention_default_24, - tintReference = { TangemTheme.colors2.graphic.neutral.primary }, - ), - ), - type = WalletNotificationType.Critical, - ) - - data class SeedPhraseSecondNotification( - val onDeclineClick: () -> Unit, - val onConfirmClick: () -> Unit, - ) : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "SeedPhraseSecondIssueNotification", - title = resourceReference(id = R.string.warning_seedphrase_action_required_title), - subtitle = resourceReference(id = R.string.warning_seedphrase_contacted_support), - messageEffect = TangemMessageEffect.Warning, - iconUM = TangemIconUM.Icon( - iconRes = R.drawable.ic_attention_default_24, - tintReference = { TangemTheme.colors2.graphic.neutral.primary }, - ), - buttonsUM = persistentListOf( - TangemMessageButtonUM( - text = resourceReference(id = R.string.seed_warning_no), - type = TangemButtonType.PrimaryInverse, - onClick = onDeclineClick, - ), - TangemMessageButtonUM( - text = resourceReference(id = R.string.seed_warning_yes), - type = TangemButtonType.PrimaryInverse, - onClick = onConfirmClick, - ), - ), - ), - type = WalletNotificationType.Critical, - ) - data class MissingBackup(val onClick: () -> Unit) : WalletNotificationUM( messageUM = TangemMessageUM( id = "MissingBackupNotification", From bbb3c9d6bb310da64184d0825bc144d78a7637f7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Mar 2026 12:08:04 +0400 Subject: [PATCH 20/75] Updated on 2026-08-14 --- .../com/tangem/scenarios/DialogScenarios.kt | 14 +++--- .../screens/ScanWarningDialogPageObject.kt | 48 +++++++++++-------- .../kotlin/com/tangem/tests/FeedbackTest.kt | 27 ++++++++--- .../com/tangem/tap/ApplicationEntryPoint.kt | 3 ++ .../java/com/tangem/tap/TangemApplication.kt | 4 ++ .../common/redux/global/GlobalMiddleware.kt | 15 +++--- .../com/tangem/tap/domain/model/Currency.kt | 17 ------- .../domain/scanCard/UseCaseScanProcessor.kt | 21 ++++---- .../tap/features/scanfails/ScanFailsModel.kt | 6 +-- .../scanfails/ui/ScanFailsDialogContent.kt | 14 +++++- .../tap/proxy/redux/DaggerGraphState.kt | 2 + .../com/tangem/tap/routing/RootContent.kt | 3 ++ .../component/impl/DefaultRoutingComponent.kt | 12 +++++ .../features/addCustomToken/CustomCurrency.kt | 27 ----------- .../redux/OnboardingManageTokensAction.kt | 7 --- .../model/OnboardingManageTokensModel.kt | 7 --- 16 files changed, 112 insertions(+), 115 deletions(-) delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/redux/OnboardingManageTokensAction.kt diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/DialogScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/DialogScenarios.kt index 57d70b597e..1bb8e36688 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/DialogScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/DialogScenarios.kt @@ -7,10 +7,10 @@ import com.tangem.screens.AlreadyUsedWalletDialogPageObject.message import com.tangem.screens.AlreadyUsedWalletDialogPageObject.requestSupportButton import com.tangem.screens.AlreadyUsedWalletDialogPageObject.thisIsMyWalletButton import com.tangem.screens.AlreadyUsedWalletDialogPageObject.title -import com.tangem.screens.ScanWarningDialogPageObject import com.tangem.screens.onActionIsUnavailableDialog import com.tangem.screens.onDataNotLoadedDialog import com.tangem.screens.onFailedTransactionDialog +import com.tangem.screens.onScanWarningDialog import io.qameta.allure.kotlin.Allure.step fun BaseTestCase.checkFailedTransactionDialog() { @@ -31,21 +31,21 @@ fun BaseTestCase.checkFailedTransactionDialog() { } } -fun checkScanWarningDialog() { +fun BaseTestCase.checkScanWarningDialog() { step("Assert 'Scan warning' dialog title is displayed") { - ScanWarningDialogPageObject { warningTitle.isDisplayed() } + onScanWarningDialog { warningTitle.assertIsDisplayed() } } step("Assert warning dialog message is displayed") { - ScanWarningDialogPageObject { warningMessage.isDisplayed() } + onScanWarningDialog { warningMessage.assertIsDisplayed() } } step("Assert 'Cancel' button is displayed") { - ScanWarningDialogPageObject { cancelButton.isDisplayed() } + onScanWarningDialog { cancelButton.assertIsDisplayed() } } step("Assert 'How to scan' button is displayed") { - ScanWarningDialogPageObject { howToScanButton.isDisplayed() } + onScanWarningDialog { howToScanButton.assertIsDisplayed() } } step("Assert 'Request support' button is displayed") { - ScanWarningDialogPageObject { requestSupportButton.isDisplayed() } + onScanWarningDialog { requestSupportButton.assertIsDisplayed() } } } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/ScanWarningDialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/ScanWarningDialogPageObject.kt index 245f08e61d..fe3ecadbd0 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/ScanWarningDialogPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/ScanWarningDialogPageObject.kt @@ -1,36 +1,42 @@ package com.tangem.screens -import com.kaspersky.kaspresso.screens.KScreen +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.tap.features.scanfails.ui.ScanFailsDialogTestTags import com.tangem.wallet.R -import io.github.kakaocup.kakao.text.KTextView -import io.github.kakaocup.kakao.text.KButton +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString -object ScanWarningDialogPageObject : KScreen() { +class ScanWarningDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { - override val layoutId: Int? = null - override val viewClass: Class<*>? = null - - val warningTitle = KTextView { - withText(R.string.common_warning) + val warningTitle: KNode = child { + hasText(getResourceString(R.string.common_warning)) + useUnmergedTree = true } - val warningMessage = KTextView { - withText(R.string.alert_troubleshooting_scan_card_title) + val warningMessage: KNode = child { + hasText(getResourceString(R.string.alert_troubleshooting_scan_card_title)) + useUnmergedTree = true } - val tryAgainButton = KButton { - withId(R.id.try_again_button) + val howToScanButton: KNode = child { + hasTestTag(ScanFailsDialogTestTags.HOW_TO_SCAN_BUTTON) + useUnmergedTree = true } - val howToScanButton = KButton { - withId(R.id.how_to_scan_button) + val requestSupportButton: KNode = child { + hasTestTag(ScanFailsDialogTestTags.REQUEST_SUPPORT_BUTTON) + useUnmergedTree = true } - val requestSupportButton = KButton { - withId(R.id.request_support_button) + val cancelButton: KNode = child { + hasTestTag(ScanFailsDialogTestTags.CANCEL_BUTTON) + useUnmergedTree = true } +} - val cancelButton = KButton { - withId(R.id.cancel_button) - } -} \ No newline at end of file +internal fun BaseTestCase.onScanWarningDialog(function: ScanWarningDialogPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt index f6d9837a42..1009e83a52 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt @@ -7,13 +7,26 @@ import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.clickAndWaitFor import com.tangem.common.extensions.clickWithAssertion -import com.tangem.domain.redux.StateDialog +import com.tangem.domain.card.ScanFailsRequester +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch import com.tangem.scenarios.checkFailedTransactionDialog import com.tangem.scenarios.checkScanWarningDialog import com.tangem.scenarios.openMainScreen import com.tangem.scenarios.synchronizeAddresses -import com.tangem.screens.* -import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.screens.ThirdPartyAppPageObject +import com.tangem.screens.onCreateWalletStartScreen +import com.tangem.screens.onDetailsScreen +import com.tangem.screens.onDisclaimerScreen +import com.tangem.screens.onFailedTransactionDialog +import com.tangem.screens.onMainScreen +import com.tangem.screens.onScanWarningDialog +import com.tangem.screens.onSendAddressScreen +import com.tangem.screens.onSendConfirmScreen +import com.tangem.screens.onSendScreen +import com.tangem.screens.onStoriesScreen +import com.tangem.screens.onTokenDetailsScreen +import com.tangem.screens.onTopBar import com.tangem.tap.domain.sdk.mocks.MockProvider import com.tangem.tap.store import dagger.hilt.android.testing.HiltAndroidTest @@ -164,8 +177,10 @@ class FeedbackTest : BaseTestCase() { } step("Force show 'Scan warning' dialog"){ runOnUiThread { - val scanFailsState = StateDialog.ScanFailsDialog(source = StateDialog.ScanFailsSource.MAIN) - store.dispatch(GlobalAction.ShowDialog(scanFailsState)) + val requester = store.state.daggerGraphState.scanFailsRequester!! + MainScope().launch { + requester.show(ScanFailsRequester.Source.MAIN) + } } } step("Check 'Scan warning' dialog") { @@ -173,7 +188,7 @@ class FeedbackTest : BaseTestCase() { checkScanWarningDialog() } step("Click on 'Request support' button") { - ScanWarningDialogPageObject { requestSupportButton.click() } + onScanWarningDialog { requestSupportButton.performClick() } } step("Assert 'Gmail' app is open") { ThirdPartyAppPageObject { assertElementWithTextExists(gmailText) } diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index f234d867d6..957e21f3dd 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -28,6 +28,7 @@ import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor +import com.tangem.domain.card.ScanFailsRequester import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.feedback.GetWalletMetaInfoUseCase @@ -151,4 +152,6 @@ interface ApplicationEntryPoint { fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory fun getCustomerIoFeatureToggles(): CustomerIoFeatureToggles + + fun getScanFailsRequester(): ScanFailsRequester } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 01efaeb7bf..e03df1316b 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -239,6 +239,9 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val customerIoFeatureToggles: CustomerIoFeatureToggles get() = entryPoint.getCustomerIoFeatureToggles() + private val scanFailsRequester + get() = entryPoint.getScanFailsRequester() + // endregion private val appScope = MainScope() @@ -368,6 +371,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. userWalletsListRepository = userWalletsListRepository, tangemHotSdk = tangemHotSdk, trackingContextProxy = trackingContextProxy, + scanFailsRequester = scanFailsRequester, ), ), ) 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 7e4af406fb..5bf0f5559a 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 @@ -4,9 +4,8 @@ import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.card.ScanFailsRequester import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.redux.StateDialog -import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState @@ -54,12 +53,14 @@ private fun handleFailureChooseBehaviour( store.dispatch(GlobalAction.ScanFailsCounter.Increment) if (store.state.globalState.scanCardFailsCounter >= 2) { val scanFailsSource = when (analyticsSource) { - is AnalyticsParam.ScreensSources.SignIn -> StateDialog.ScanFailsSource.SIGN_IN - is AnalyticsParam.ScreensSources.Settings -> StateDialog.ScanFailsSource.SETTINGS - is AnalyticsParam.ScreensSources.Intro -> StateDialog.ScanFailsSource.INTRO - else -> StateDialog.ScanFailsSource.MAIN + is AnalyticsParam.ScreensSources.SignIn -> ScanFailsRequester.Source.SIGN_IN + is AnalyticsParam.ScreensSources.Settings -> ScanFailsRequester.Source.SETTINGS + is AnalyticsParam.ScreensSources.Intro -> ScanFailsRequester.Source.INTRO + else -> ScanFailsRequester.Source.MAIN + } + scope.launch { + store.inject(DaggerGraphState::scanFailsRequester).show(scanFailsSource) } - store.dispatchDialogShow(StateDialog.ScanFailsDialog(scanFailsSource)) } } else { store.dispatch(GlobalAction.ScanFailsCounter.Reset) diff --git a/app/src/main/java/com/tangem/tap/domain/model/Currency.kt b/app/src/main/java/com/tangem/tap/domain/model/Currency.kt index 1155c698d4..29c89811fc 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/Currency.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/Currency.kt @@ -1,6 +1,5 @@ package com.tangem.tap.domain.model -import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.blockchain.common.Blockchain as SdkBlockchain import com.tangem.blockchain.common.Token as SdkToken @@ -29,20 +28,4 @@ sealed interface Currency { ) : Currency { override val currencySymbol: CryptoCurrencyName = blockchain.currency } - - companion object { - fun fromCustomCurrency(customCurrency: CustomCurrency): Currency { - return when (customCurrency) { - is CustomCurrency.CustomBlockchain -> Blockchain( - blockchain = customCurrency.network, - derivationPath = customCurrency.derivationPath?.rawPath, - ) - is CustomCurrency.CustomToken -> Token( - token = customCurrency.token, - blockchain = customCurrency.network, - derivationPath = customCurrency.derivationPath?.rawPath, - ) - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt index 6076dd07d3..f82939019e 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt @@ -9,18 +9,19 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.domain.card.ScanCardException +import com.tangem.domain.card.ScanFailsRequester import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent -import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.inject import com.tangem.tap.domain.scanCard.chains.* import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter import com.tangem.tap.proxy.redux.DaggerGraphState +import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.delay +import kotlinx.coroutines.launch internal object UseCaseScanProcessor { private val scanCardExceptionConverter = ScanCardExceptionConverter() @@ -57,7 +58,7 @@ internal object UseCaseScanProcessor { val chains = buildList { add( FailedScansCounterChain( - { showMaxUnsuccessfulScansReachedDialog(analyticsSource) }, + { showScanFailsDialog(analyticsSource) }, ), ) add(AnalyticsChain(Basic.CardWasScanned(analyticsSource))) @@ -71,14 +72,16 @@ internal object UseCaseScanProcessor { ) } - private fun showMaxUnsuccessfulScansReachedDialog(source: AnalyticsParam.ScreensSources) { + private fun showScanFailsDialog(source: AnalyticsParam.ScreensSources) { val scanFailsSource = when (source) { - is AnalyticsParam.ScreensSources.SignIn -> StateDialog.ScanFailsSource.SIGN_IN - is AnalyticsParam.ScreensSources.Settings -> StateDialog.ScanFailsSource.SETTINGS - is AnalyticsParam.ScreensSources.Intro -> StateDialog.ScanFailsSource.INTRO - else -> StateDialog.ScanFailsSource.MAIN + is AnalyticsParam.ScreensSources.SignIn -> ScanFailsRequester.Source.SIGN_IN + is AnalyticsParam.ScreensSources.Settings -> ScanFailsRequester.Source.SETTINGS + is AnalyticsParam.ScreensSources.Intro -> ScanFailsRequester.Source.INTRO + else -> ScanFailsRequester.Source.MAIN + } + scope.launch { + store.inject(DaggerGraphState::scanFailsRequester).show(scanFailsSource) } - store.dispatchDialogShow(StateDialog.ScanFailsDialog(scanFailsSource)) } private suspend fun proceedWithException( diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsModel.kt b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsModel.kt index e94e714d65..bbd95fded5 100644 --- a/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsModel.kt @@ -13,11 +13,7 @@ import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics import com.tangem.tap.features.scanfails.ui.ScanFailsUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsDialogContent.kt b/app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsDialogContent.kt index fea911a528..3e7c5f3621 100644 --- a/app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsDialogContent.kt +++ b/app/src/main/java/com/tangem/tap/features/scanfails/ui/ScanFailsDialogContent.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -61,34 +62,43 @@ internal fun ScanFailsDialogContent(state: ScanFailsUM) { text = stringResourceSafe(R.string.alert_button_how_to_scan), onClick = state.onHowToScan, color = TangemTheme.colors.text.accent, + modifier = Modifier.testTag(ScanFailsDialogTestTags.HOW_TO_SCAN_BUTTON), ) DialogTextButton( text = stringResourceSafe(R.string.alert_button_request_support), onClick = state.onRequestSupport, color = TangemTheme.colors.text.accent, + modifier = Modifier.testTag(ScanFailsDialogTestTags.REQUEST_SUPPORT_BUTTON), ) DialogTextButton( text = stringResourceSafe(R.string.common_cancel), onClick = state.onDismiss, color = TangemTheme.colors.text.warning, + modifier = Modifier.testTag(ScanFailsDialogTestTags.CANCEL_BUTTON), ) } } } @Composable -private fun DialogTextButton(text: String, onClick: () -> Unit, color: Color) { +private fun DialogTextButton(text: String, onClick: () -> Unit, color: Color, modifier: Modifier = Modifier) { Text( text = text.uppercase(), color = color, style = TangemTheme.typography.button, textAlign = TextAlign.Center, - modifier = Modifier.clickable(onClick = onClick), + modifier = modifier.clickable(onClick = onClick), ) } +object ScanFailsDialogTestTags { + const val HOW_TO_SCAN_BUTTON = "scan_fails_how_to_scan_button" + const val REQUEST_SUPPORT_BUTTON = "scan_fails_request_support_button" + const val CANCEL_BUTTON = "scan_fails_cancel_button" +} + // region Preview @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) 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 861d425898..9f31ff291c 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 @@ -18,6 +18,7 @@ import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.ScanCardUseCase +import com.tangem.domain.card.ScanFailsRequester import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.wallets.UserWalletsListRepository @@ -73,4 +74,5 @@ data class DaggerGraphState( val userWalletsListRepository: UserWalletsListRepository? = null, val tangemHotSdk: TangemHotSdk? = null, val trackingContextProxy: TrackingContextProxy? = null, + val scanFailsRequester: ScanFailsRequester? = null, ) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/RootContent.kt b/app/src/main/java/com/tangem/tap/routing/RootContent.kt index 3c941cecd3..a2701684bf 100644 --- a/app/src/main/java/com/tangem/tap/routing/RootContent.kt +++ b/app/src/main/java/com/tangem/tap/routing/RootContent.kt @@ -52,6 +52,7 @@ internal fun RootContent( wcContent: @Composable (modifier: Modifier) -> Unit, hotAccessCodeContent: @Composable (modifier: Modifier) -> Unit, rootDetectedWarningContent: @Composable (modifier: Modifier) -> Unit, + scanFailsContent: @Composable (modifier: Modifier) -> Unit, ) { val context = LocalContext.current @@ -89,6 +90,8 @@ internal fun RootContent( rootDetectedWarningContent(Modifier.fillMaxSize()) + scanFailsContent(Modifier.fillMaxSize()) + TangemSnackbarHost( modifier = Modifier .align(Alignment.BottomCenter) diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index fb71667669..d922ce744d 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -40,6 +40,8 @@ import com.tangem.hot.sdk.android.create import com.tangem.sdk.api.BackupServiceHolder import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.analytics.events.Onboarding +import com.tangem.tap.features.scanfails.ScanFailsComponent +import com.tangem.tap.features.scanfails.ScanFailsRequesterProxy import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.features.hot.TangemHotSDKProxy import com.tangem.tap.features.root.RootDetectedWarningComponent @@ -75,6 +77,8 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private val cardRepository: CardRepository, private val onboardingRepository: OnboardingRepository, private val trackingContextProxy: TrackingContextProxy, + private val scanFailsComponentFactory: ScanFailsComponent.Factory, + private val scanFailsRequesterProxy: ScanFailsRequesterProxy, private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val backupServiceHolder: BackupServiceHolder, @@ -97,6 +101,11 @@ internal class DefaultRoutingComponent @AssistedInject constructor( .create(child("rootDetectedWarningComponent"), Unit) } + private val scanFailsComponent: ScanFailsComponent by lazy { + scanFailsComponentFactory + .create(child("scanFailsComponent"), Unit) + } + private val navigation = navigationProvider.getOrCreateTyped() private val stack: Value> = childStack( @@ -197,6 +206,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( wcContent = { wcRoutingComponent.Content(it) }, hotAccessCodeContent = { hotAccessCodeRequestComponent.Content(it) }, rootDetectedWarningContent = { rootDetectedWarningComponent.Content(it) }, + scanFailsContent = { scanFailsComponent.Content(it) }, ) } @@ -240,10 +250,12 @@ internal class DefaultRoutingComponent @AssistedInject constructor( onCreate = { tangemHotSDKProxy.sdkState.value = TangemHotSdk.create(activity) hotAccessCodeRequesterProxy.componentRequester.value = hotAccessCodeRequestComponent + scanFailsRequesterProxy.componentRequester.value = scanFailsComponent }, onDestroy = { tangemHotSDKProxy.sdkState.value = null hotAccessCodeRequesterProxy.componentRequester.value = null + scanFailsRequesterProxy.componentRequester.value = null }, ) } diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt deleted file mode 100644 index 15debdd88f..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.domain.features.addCustomToken - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.crypto.hdWallet.DerivationPath - -/** -[REDACTED_AUTHOR] - */ -sealed class CustomCurrency( - val network: Blockchain, - val derivationPath: DerivationPath?, -) { - - @Deprecated("It will be removed in next releases") - class CustomBlockchain( - network: Blockchain, - derivationPath: DerivationPath?, - ) : CustomCurrency(network, derivationPath) - - @Deprecated("It will be removed in next releases") - class CustomToken( - val token: Token, - network: Blockchain, - derivationPath: DerivationPath?, - ) : CustomCurrency(network, derivationPath) -} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/OnboardingManageTokensAction.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/OnboardingManageTokensAction.kt deleted file mode 100644 index 1aab9469b3..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/OnboardingManageTokensAction.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.tangem.domain.redux - -import org.rekotlin.Action - -sealed class OnboardingManageTokensAction : Action { - data object CurrenciesSaved : OnboardingManageTokensAction() -} \ No newline at end of file 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 4010fb57e9..2e0e1b9a56 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 @@ -13,8 +13,6 @@ 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.redux.OnboardingManageTokensAction -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent import com.tangem.features.managetokens.component.ManageTokensMode import com.tangem.features.managetokens.component.ManageTokensSource @@ -41,7 +39,6 @@ import javax.inject.Inject internal class OnboardingManageTokensModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val messageSender: UiMessageSender, - private val reduxStateHolder: ReduxStateHolder, private val analyticsEventHandler: AnalyticsEventHandler, manageTokensListManagerFactory: ManageTokensListManager.Factory, manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory, @@ -294,10 +291,6 @@ internal class OnboardingManageTokensModel @Inject constructor( } private fun returnToParentComponent() { - // old onboarding - reduxStateHolder.dispatch(OnboardingManageTokensAction.CurrenciesSaved) - - // new one modelScope.launch { returnToParentComponentFlow.emit(Unit) } From 60ba58154025b477788dd5d88af2c500526a7a6f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Mar 2026 08:54:52 +0300 Subject: [PATCH 21/75] Updated on 2026-08-14 --- .../tangem/common/ui/charts/MarketChart.kt | 115 +++++++++++++++--- .../common/ui/charts/state/MarketChartLook.kt | 4 +- .../ui/charts/state/MarketChartState.kt | 5 +- .../tangem/core/ui/res/TangemThemeRedesign.kt | 1 + .../details/MarketsTokenDetailsModel.kt | 1 + gradle/tangem_dependencies.toml | 2 +- 6 files changed, 106 insertions(+), 22 deletions(-) diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt index 52f967ed67..614776c6b8 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/MarketChart.kt @@ -24,20 +24,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost -import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisGuidelineComponent -import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent -import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottomAxis -import com.patrykandpatrick.vico.compose.cartesian.axis.rememberCustomStartAxis +import com.patrykandpatrick.vico.compose.cartesian.axis.* import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart import com.patrykandpatrick.vico.compose.cartesian.rememberVicoScrollState import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState import com.patrykandpatrick.vico.compose.common.of import com.patrykandpatrick.vico.core.cartesian.HorizontalLayout import com.patrykandpatrick.vico.core.cartesian.Zoom -import com.patrykandpatrick.vico.core.cartesian.axis.AxisPosition -import com.patrykandpatrick.vico.core.cartesian.axis.BaseAxis -import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis -import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis +import com.patrykandpatrick.vico.core.cartesian.axis.* import com.patrykandpatrick.vico.core.cartesian.data.AxisValueOverrider import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter import com.patrykandpatrick.vico.core.cartesian.marker.CartesianMarker @@ -54,6 +48,7 @@ import com.tangem.common.ui.charts.state.* import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.res.LocalHapticManager +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.utils.DateTimeFormatters @@ -98,14 +93,26 @@ fun MarketChart( val marker = rememberTangemChartMarker(color = state.chartColor) - val chart = rememberCartesianChart( - layer, - startAxis = rememberMarketChartStartAxis(state.yValueFormatter), - bottomAxis = rememberMarketChartBottomAxis(state.xValueFormatter), - horizontalLayout = HorizontalLayout.FullWidth(), - markerVisibilityListener = rememberMarketVisibilityListener(canvasWidth, state), - marker = marker, - ) + val chart = if (state.isMinMaxLook) { + rememberCartesianChart( + layer, + startAxis = rememberMarketChartStartMinMaxAxis(state.yValueFormatter), + endAxis = rememberMarketChartEndAxis(), + bottomAxis = rememberMarketChartBottomAxis(state.xValueFormatter), + horizontalLayout = HorizontalLayout.FullWidth(), + markerVisibilityListener = rememberMarketVisibilityListener(canvasWidth, state), + marker = marker, + ) + } else { + rememberCartesianChart( + layer, + startAxis = rememberMarketChartStartAxis(state.yValueFormatter), + bottomAxis = rememberMarketChartBottomAxis(state.xValueFormatter), + horizontalLayout = HorizontalLayout.FullWidth(), + markerVisibilityListener = rememberMarketVisibilityListener(canvasWidth, state), + marker = marker, + ) + } // we need to calculate what the overall height should be in order to get the correct height of the graph val bottomAxisHeight = with(LocalDensity.current) { @@ -180,6 +187,62 @@ private fun rememberMarketVisibilityListener( } } +@Composable +private fun rememberMarketChartEndAxis(): VerticalAxis { + return rememberEndAxis( + line = null, + tick = null, + guideline = rememberChartAxisGuidelineComponent( + color = TangemTheme.colors.icon.inactive.copy(alpha = 0.12f), + isWithPadding = false, + ), + label = null, + horizontalLabelPosition = VerticalAxis.HorizontalLabelPosition.Inside, + verticalLabelPosition = VerticalAxis.VerticalLabelPosition.Center, + itemPlacer = MidVerticalAxisItemPlacer(false), + valueFormatter = CartesianValueFormatter.yPercent(), + ) +} + +@Composable +private fun rememberMarketChartStartMinMaxAxis( + yValueFormatter: CartesianValueFormatter, +): VerticalAxis { + val textStyle = TangemTheme.typography.caption2 + val resolver = LocalFontFamilyResolver.current + val typeface by remember(resolver, textStyle) { + resolver.resolveAsTypeface( + fontFamily = textStyle.fontFamily, + fontWeight = textStyle.fontWeight ?: FontWeight.Normal, + fontStyle = textStyle.fontStyle ?: FontStyle.Normal, + fontSynthesis = textStyle.fontSynthesis ?: FontSynthesis.All, + ) + } + + return rememberMinMaxStartAxis( + line = null, + tick = null, + guideline = null, + labelGuideline = rememberChartAxisGuidelineComponent( + color = TangemTheme.colors.icon.inactive.copy(alpha = 0.12f), + ), + label = rememberAxisLabelComponent( + color = TangemTheme.colors.text.tertiary, + background = null, + padding = Dimensions.of( + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + textSize = TangemTheme.typography.caption2.fontSize, + typeface = typeface, + ), + horizontalLabelPosition = VerticalAxis.HorizontalLabelPosition.Inside, + verticalLabelPosition = VerticalAxis.VerticalLabelPosition.Center, + itemPlacer = VerticalAxis.ItemPlacer.count({ 2 }, false), + valueFormatter = yValueFormatter, + ) +} + @Composable private fun rememberMarketChartStartAxis( yValueFormatter: CartesianValueFormatter, @@ -253,13 +316,27 @@ private fun rememberMarketChartBottomAxis( } @Composable -private fun rememberChartAxisGuidelineComponent(color: Color): LineComponent { +private fun rememberChartAxisGuidelineComponent(color: Color, isWithPadding: Boolean = true): LineComponent { + val isRedesign = LocalRedesignEnabled.current + + val startPadding = if (isRedesign) { + TangemTheme.dimens2.x2_5 + } else { + TangemTheme.dimens.spacing4 + } + + val endPadding = if (isRedesign) { + TangemTheme.dimens2.x2 + } else { + TangemTheme.dimens.spacing4 + } + return rememberAxisGuidelineComponent( color = color, shape = Shape.Rectangle, margins = Dimensions( - startDp = TangemTheme.dimens.spacing4.value, - endDp = TangemTheme.dimens.spacing4.value, + startDp = if (isWithPadding) startPadding.value else 0f, + endDp = if (isWithPadding) endPadding.value else 0f, topDp = 0f, bottomDp = 0f, ), diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt index 2b7599b033..9a99817e32 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartLook.kt @@ -6,9 +6,10 @@ import com.tangem.common.ui.charts.state.formatter.AxisLabelFormatter /** * This class represents the look and feel of a Market Chart. * It includes properties for type, marker highlight, animation on data change, animate data appearance, - * and formatters for x and y axis. + * and formatters for x and y-axis. * * @property type The type of the chart, can be either Growing or Falling. + * @property isMinMaxLook A boolean indicating whether the chart should contain markers for minimum and maximum values. * @property shouldMarkerHighlightRightSide A boolean indicating whether the marker highlights the right side of the chart. * @property xAxisFormatter A formatter for the x-axis labels. * @property yAxisFormatter A formatter for the y-axis labels. @@ -16,6 +17,7 @@ import com.tangem.common.ui.charts.state.formatter.AxisLabelFormatter @Immutable data class MarketChartLook( val type: Type = Type.Growing, + val isMinMaxLook: Boolean = true, val shouldMarkerHighlightRightSide: Boolean = true, val xAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() }, val yAxisFormatter: AxisLabelFormatter = AxisLabelFormatter { it.toString() }, diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt index 62c312b4b4..f38849d405 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartState.kt @@ -51,7 +51,6 @@ fun rememberMarketChartState( * @property lookState The look state of the Market Chart. * @property colorMapper A function that maps a MarketChartLook.Type to a Color. * @property markerCallback A callback function that is called when the marker is shown, hidden, or updated. - * @property isDrawingAnimationInProgress A boolean indicating whether the drawing animation is in progress. */ @Stable class MarketChartState internal constructor( @@ -70,6 +69,10 @@ class MarketChartState internal constructor( lookState.value.shouldMarkerHighlightRightSide } + internal val isMinMaxLook by derivedStateOf { + lookState.value.isMinMaxLook + } + internal val xValueFormatter = CartesianValueFormatter { value, _, _ -> val formatter = dataProducer.lookState.value.xAxisFormatter diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt index 51aacf9640..adac73f3c7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt @@ -31,6 +31,7 @@ fun TangemThemeRedesign(content: @Composable () -> Unit) { LocalTangemColors provides themeColors, LocalTangemColors2 provides if (LocalIsInDarkTheme.current) darkThemeColors2() else lightThemeColors2(), LocalTangemTypography2 provides TangemTypography2(InterFamily), + LocalTangemTypography provides TangemTypography(InterFamily), LocalRootBackgroundColor provides remember(rootBackgroundColor) { mutableStateOf(rootBackgroundColor) }, ) { CompositionLocalProvider( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt index f362ecfdab..eb07937be6 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt @@ -189,6 +189,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( marketChartLook.copy( type = percentChangeType.toChartType(), + isMinMaxLook = designFeatureToggles.isRedesignEnabled, xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(PriceChangeInterval.H24), yAxisFormatter = { value -> value.format { diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 40a6e10dec..8ff57751af 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -9,7 +9,7 @@ tangemBlockchainSdk = "develop-1455" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-598" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ -tangemVico = "2.0.0-alpha.25-tangem12" +tangemVico = "tangem-master-21" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ tangemHotSdk = "develop-549" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 88f720529364c9d525aa391dc3489e23ffdb9802 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Mar 2026 16:03:41 +0400 Subject: [PATCH 22/75] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 5 -- .../com/tangem/tap/common/DialogManager.kt | 52 ------------ .../com/tangem/tap/common/extensions/Store.kt | 33 -------- .../tap/common/redux/global/GlobalAction.kt | 5 -- .../tap/common/redux/global/GlobalReducer.kt | 6 -- .../tap/common/redux/global/GlobalState.kt | 10 +-- .../tangem/tap/common/ui/ScanFailsDialog.kt | 84 ------------------- .../features/onboarding/OnboardingDialog.kt | 10 --- .../ui/dialogs/WalletActivationErrorDialog.kt | 45 ---------- .../com/tangem/tap/proxy/AppStateHolder.kt | 6 -- app/src/main/res/layout/dialog_scan_fails.xml | 84 ------------------- .../tangem/domain/redux/ReduxStateHolder.kt | 2 - .../com/tangem/domain/redux/StateDialog.kt | 10 --- .../router/DefaultWalletRouter.kt | 7 -- .../presentation/router/InnerWalletRouter.kt | 3 - 15 files changed, 1 insertion(+), 361 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/DialogManager.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt delete mode 100644 app/src/main/res/layout/dialog_scan_fails.xml delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 2d3a35f48c..5372f06248 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -54,7 +54,6 @@ import com.tangem.operations.backup.BackupService import com.tangem.sdk.api.BackupServiceHolder import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.common.ActivityResultCallbackHolder -import com.tangem.tap.common.DialogManager import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.analytics.events.Push import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder @@ -171,8 +170,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { private lateinit var appThemeModeFlow: SharedFlow - private val dialogManager = DialogManager() - private val onActivityResultCallbacks = mutableListOf() override fun onCreate(savedInstanceState: Bundle?) { @@ -324,11 +321,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { override fun onStart() { super.onStart() TangemLogger.i("onStart") - dialogManager.onStart(this) } override fun onStop() { - dialogManager.onStop() super.onStop() TangemLogger.i("onStop") } diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt deleted file mode 100644 index 7c984b6d60..0000000000 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.tap.common - -import android.app.Dialog -import android.content.Context -import com.tangem.domain.redux.StateDialog -import com.tangem.tap.common.redux.global.GlobalState -import com.tangem.tap.common.ui.ScanFailsDialog -import com.tangem.tap.features.onboarding.OnboardingDialog -import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletActivationErrorDialog -import com.tangem.tap.store -import org.rekotlin.StoreSubscriber - -class DialogManager : StoreSubscriber { - var context: Context? = null - private var dialog: Dialog? = null - - fun onStart(context: Context) { - this.context = context - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.globalState == newState.globalState - }.select { it.globalState } - } - } - - fun onStop() { - this.context = null - store.unsubscribe(this) - } - - @Suppress("LongMethod", "ComplexMethod") - override fun newState(state: GlobalState) { - if (state.dialog == null) { - dialog?.dismiss() - dialog = null - return - } - val context = context ?: return - if (dialog != null) return - - dialog = when (state.dialog) { - is StateDialog.ScanFailsDialog -> ScanFailsDialog.create( - context = context, - source = state.dialog.source, - onTryAgain = state.dialog.onTryAgain, - ) - is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog) - else -> null - } - dialog?.show() - } -} \ No newline at end of file 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 62bf0eb950..8d38cb9f3a 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 @@ -1,14 +1,9 @@ package com.tangem.tap.common.extensions import com.tangem.common.routing.AppRouter -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.TapError import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store @@ -45,26 +40,6 @@ suspend fun Store.onUserWalletSelected(userWallet: UserWallet) { state.globalState.tapWalletManager.onWalletSelected(userWallet) } -/** - * @param fatal used to indicate errors that should not normally occur - */ -fun Store.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) { - val prefix = if (fatal) "FATAL ERROR: " else "DEBUG ERROR: " - dispatchDebugErrorNotification(TapError.CustomError("$prefix $message")) -} - -fun Store.dispatchDebugErrorNotification(error: TapError) { - inject(DaggerGraphState::uiMessageSender).send(SnackbarMessage(stringReference(error.message ?: "debug error"))) -} - -fun Store<*>.dispatchDialogShow(dialog: StateDialog) { - dispatchOnMain(GlobalAction.ShowDialog(dialog)) -} - -fun Store<*>.dispatchDialogHide() { - dispatchOnMain(GlobalAction.HideDialog) -} - /** * Dispatch action inside a coroutine with the Main dispatcher */ @@ -76,14 +51,6 @@ suspend fun dispatchOnMain(vararg actions: Action) { withMainContext { actions.forEach { store.dispatch(it) } } } -fun Store.dispatchOpenUrl(url: String) { - inject(DaggerGraphState::urlOpener).openUrl(url) -} - -fun Store.dispatchShare(url: String) { - inject(DaggerGraphState::shareManager).shareText(url) -} - fun Store.dispatchNavigationAction(action: AppRouter.() -> Unit) { inject(DaggerGraphState::appRouter).action() } 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 404c7150f4..315fe98387 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 @@ -4,15 +4,10 @@ import com.tangem.common.CompletionResult import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.redux.StateDialog import org.rekotlin.Action sealed class GlobalAction : Action { - // dialogs - data class ShowDialog(val stateDialog: StateDialog) : GlobalAction() - object HideDialog : GlobalAction() - object ScanFailsCounter { data class ChooseBehavior( val result: CompletionResult, 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 a1ed71be61..b381df3054 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 @@ -26,12 +26,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState { globalState.copy(appCurrency = action.appCurrency) } is GlobalAction.IsSignWithRing -> globalState.copy(isLastSignWithRing = action.isSignWithRing) - is GlobalAction.ShowDialog -> { - globalState.copy(dialog = action.stateDialog) - } - is GlobalAction.HideDialog -> { - globalState.copy(dialog = null) - } else -> globalState } } \ No newline at end of file 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 598ad3be24..3c7906a1e5 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 @@ -2,24 +2,16 @@ package com.tangem.tap.common.redux.global import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.redux.StateDialog import com.tangem.tap.domain.TapWalletManager import org.rekotlin.StateType data class GlobalState( @Deprecated("Use scan response from selected user wallet") val scanResponse: ScanResponse? = null, - val onboardingState: OnboardingState = OnboardingState(), val tapWalletManager: TapWalletManager = TapWalletManager(), val appCurrency: AppCurrency = AppCurrency.Default, val scanCardFailsCounter: Int = 0, - val dialog: StateDialog? = null, val isLastSignWithRing: Boolean = false, ) : StateType -typealias CryptoCurrencyName = String - -data class OnboardingState( - val isOnboardingStarted: Boolean = false, - val shouldResetOnCreate: Boolean = false, -) \ No newline at end of file +typealias CryptoCurrencyName = String \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt deleted file mode 100644 index 61b24a7d63..0000000000 --- a/app/src/main/java/com/tangem/tap/common/ui/ScanFailsDialog.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tangem.tap.common.ui - -import android.content.Context -import android.view.View -import android.widget.TextView -import androidx.appcompat.app.AlertDialog -import androidx.compose.ui.text.intl.Locale -import androidx.core.view.isVisible -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic -import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.redux.StateDialog -import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics -import com.tangem.tap.common.extensions.dispatchDialogHide -import com.tangem.tap.common.extensions.dispatchOpenUrl -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.wallet.R -import kotlinx.coroutines.launch - -/** -[REDACTED_AUTHOR] - */ -internal object ScanFailsDialog { - - private const val HOW_TO_SCAN_RU_LINK = "https://tangem.com/ru/blog/post/scan-tangem-card/" - private const val HOW_TO_SCAN_LINK = "https://tangem.com/en/blog/post/scan-tangem-card/" - private const val RUSSIA_LOCALE = "ru" - - fun create(context: Context, source: StateDialog.ScanFailsSource, onTryAgain: (() -> Unit)? = null): AlertDialog { - return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply { - val customView = View.inflate(context, R.layout.dialog_scan_fails, null) - val sourceAnalytics = when (source) { - StateDialog.ScanFailsSource.MAIN -> AnalyticsParam.ScreensSources.Main - StateDialog.ScanFailsSource.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn - StateDialog.ScanFailsSource.SETTINGS -> AnalyticsParam.ScreensSources.Settings - StateDialog.ScanFailsSource.INTRO -> AnalyticsParam.ScreensSources.Intro - } - val tryAgainBtn: TextView? = customView.findViewById(R.id.try_again_button) - if (onTryAgain != null) { - tryAgainBtn?.isVisible = true - tryAgainBtn?.setOnClickListener { - store.dispatchDialogHide() - Analytics.send( - ScanFailsDialogAnalytics( - button = ScanFailsDialogAnalytics.Buttons.TRY_AGAIN, - source = sourceAnalytics, - ), - ) - onTryAgain() - } - } else { - tryAgainBtn?.isVisible = false - } - customView.findViewById(R.id.how_to_scan_button)?.setOnClickListener { - Analytics.send( - ScanFailsDialogAnalytics( - button = ScanFailsDialogAnalytics.Buttons.HOW_TO_SCAN, - source = sourceAnalytics, - ), - ) - val locale = Locale.current.region - val link = if (locale.lowercase() == RUSSIA_LOCALE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK - store.dispatchOpenUrl(link) - } - customView.findViewById(R.id.request_support_button)?.setOnClickListener { - Analytics.send(Basic.ButtonSupport(sourceAnalytics)) - - scope.launch { - store.inject(DaggerGraphState::sendFeedbackEmailUseCase) - .invoke(type = FeedbackEmailType.ScanningProblem) - } - } - customView.findViewById(R.id.cancel_button)?.setOnClickListener { - store.dispatchDialogHide() - } - setView(customView) - setOnDismissListener { store.dispatchDialogHide() } - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt deleted file mode 100644 index e2de5c257b..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.tap.features.onboarding - -import com.tangem.domain.redux.StateDialog - -/** -[REDACTED_AUTHOR] - */ -sealed class OnboardingDialog : StateDialog { - data class WalletActivationError(val onConfirm: () -> Unit) : OnboardingDialog() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt deleted file mode 100644 index 880f9177a0..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/WalletActivationErrorDialog.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs - -import android.app.Dialog -import android.content.Context -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic -import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.tap.common.extensions.dispatchDialogHide -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.features.onboarding.OnboardingDialog -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.wallet.R -import kotlinx.coroutines.launch - -object WalletActivationErrorDialog { - - fun create(context: Context, dialog: OnboardingDialog.WalletActivationError): Dialog { - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(context.getString(R.string.onboarding_activation_error_title)) - setMessage(context.getString(R.string.onboarding_activation_error_message)) - setPositiveButton(R.string.common_ok) { _, _ -> dialog.onConfirm() } - setNegativeButton(R.string.common_support) { _, _ -> - // changed on email support [REDACTED_TASK_KEY] - Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro)) - - val scanResponse = store.state.globalState.scanResponse - ?: error("ScanResponse must be not null") - - val cardInfo = store.inject(DaggerGraphState::getWalletMetaInfoUseCase).invoke(scanResponse).getOrNull() - ?: error("CardInfo must be not null") - - scope.launch { - store.inject(DaggerGraphState::sendFeedbackEmailUseCase) - .invoke(type = FeedbackEmailType.DirectUserRequest(cardInfo)) - } - } - setOnDismissListener { store.dispatchDialogHide() } - setCancelable(false) - }.create() - } -} \ 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 c3261b1873..e85f806bdc 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -2,8 +2,6 @@ package com.tangem.tap.proxy import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.redux.StateDialog -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 @@ -32,8 +30,4 @@ class AppStateHolder @Inject constructor() : ReduxStateHolder { override suspend fun onUserWalletSelected(userWallet: UserWallet) { mainStore?.onUserWalletSelected(userWallet) } - - override fun dispatchDialogShow(dialog: StateDialog) { - mainStore?.dispatchDialogShow(dialog) - } } \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_scan_fails.xml b/app/src/main/res/layout/dialog_scan_fails.xml deleted file mode 100644 index dd5924f8c5..0000000000 --- a/app/src/main/res/layout/dialog_scan_fails.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt index f955b2d658..3a96226153 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt @@ -10,6 +10,4 @@ interface ReduxStateHolder { suspend fun dispatchWithMain(action: Action) suspend fun onUserWalletSelected(userWallet: UserWallet) - - fun dispatchDialogShow(dialog: StateDialog) } \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt deleted file mode 100644 index 5299850377..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/StateDialog.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.domain.redux - -interface StateDialog { - - data class ScanFailsDialog(val source: ScanFailsSource, val onTryAgain: (() -> Unit)? = null) : StateDialog - - enum class ScanFailsSource { - MAIN, SIGN_IN, SETTINGS, INTRO - } -} \ No newline at end of file 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 decb5b77df..95c94d246c 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 @@ -17,8 +17,6 @@ import com.tangem.domain.qrscanning.models.QrSendTarget import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayDetailsConfig -import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.redux.StateDialog import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.tokens.model.details.TokenAction import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent @@ -35,7 +33,6 @@ import javax.inject.Inject internal class DefaultWalletRouter @Inject constructor( private val router: AppRouter, private val urlOpener: UrlOpener, - private val reduxStateHolder: ReduxStateHolder, private val designFeatureToggles: DesignFeatureToggles, ) : InnerWalletRouter { @@ -117,10 +114,6 @@ internal class DefaultWalletRouter @Inject constructor( return router.stack.lastOrNull() is AppRoute.Wallet } - override fun openScanFailedDialog(onTryAgain: () -> Unit) { - reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN, onTryAgain)) - } - override fun openNFT(userWallet: UserWallet) { router.push( AppRoute.NFT( 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 56f3e54725..fb55f96999 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 @@ -66,9 +66,6 @@ internal interface InnerWalletRouter { /** Is wallet last screen */ fun isWalletLastScreen(): Boolean - /** Open scan failed dialog */ - fun openScanFailedDialog(onTryAgain: () -> Unit) - /** Open NFT collections screen */ fun openNFT(userWallet: UserWallet) From f9f2ac60d00900d86c5bb151719f8789d5dc620a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Mar 2026 11:03:16 +0100 Subject: [PATCH 23/75] Updated on 2026-08-14 --- .../response/TokenMarketInfoResponse.kt | 2 + core/res/src/main/res/values/strings.xml | 1 + .../tangem/core/ui/res/TangemTypography2.kt | 12 + .../converters/TokenMarketInfoConverter.kt | 1 + .../tangem/domain/markets/TokenMarketInfo.kt | 1 + .../details/converter/MetricsConverter.kt | 81 ++++--- .../detailed/components/MetricsCards.kt | 213 +++++++++++------- .../ui/market/detailed/state/MetricsUM.kt | 26 ++- 8 files changed, 221 insertions(+), 116 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt index 0fb1f95598..8472387cb9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt @@ -104,6 +104,8 @@ data class TokenMarketInfoResponse( data class Metrics( @Json(name = "market_rating") val marketRating: Int?, + @Json(name = "market_rating_change_24h") + val marketRatingChange24h: Int?, @Json(name = "circulating_supply") val circulatingSupply: BigDecimal?, @Json(name = "market_cap") diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 02c1920d57..1823294875 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -834,6 +834,7 @@ The maximum number of coins or tokens that can ever exist for a particular cryptocurrency Max supply Metrics + No limited Official links Price performance Repository diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography2.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography2.kt index 4973fefe6f..8404f3a578 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography2.kt @@ -114,6 +114,18 @@ class TangemTypography2 internal constructor( ), ) + val headingSemibold22: TextStyle = TextStyle( + fontFamily = fontFamily, + fontSize = 22.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = TextUnit(value = 0.38f, type = TextUnitType.Sp), + lineHeight = TextUnit(value = 28f, type = TextUnitType.Sp), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), + ) + val headingRegular20: TextStyle = TextStyle( fontFamily = fontFamily, fontSize = 20.sp, diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt index 347370d0a9..8a6cee1f3f 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt @@ -105,6 +105,7 @@ internal class TokenMarketInfoConverter( private fun TokenMarketInfoResponse.Metrics.convert(): TokenMarketInfo.Metrics { return TokenMarketInfo.Metrics( marketRating = marketRating, + marketRatingChange24h = marketRatingChange24h, circulatingSupply = circulatingSupply, marketCap = marketCap, volume24h = volume24h, diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt index bbd7a840ce..3c1b735d86 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketInfo.kt @@ -49,6 +49,7 @@ data class TokenMarketInfo( data class Metrics( val marketRating: Int?, + val marketRatingChange24h: Int?, val circulatingSupply: BigDecimal?, val marketCap: BigDecimal?, val volume24h: BigDecimal?, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/converter/MetricsConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/converter/MetricsConverter.kt index 76cd1bed72..3422433ee1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/converter/MetricsConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/converter/MetricsConverter.kt @@ -1,6 +1,7 @@ package com.tangem.features.feed.model.market.details.converter import androidx.compose.runtime.Stable +import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList @@ -37,7 +38,7 @@ internal class MetricsConverter( metrics = persistentListOf( InfoPointUM( title = resourceReference(R.string.markets_token_details_market_capitalization), - value = marketCap.formatAmount(), + value = marketCap.formatAmount() ?: StringsSigns.DASH_SIGN, onInfoClick = { onInfoClick( InfoBottomSheetContent( @@ -65,7 +66,7 @@ internal class MetricsConverter( ), InfoPointUM( title = resourceReference(R.string.markets_token_details_trading_volume), - value = volume24h.formatAmount(), + value = volume24h.formatAmount() ?: StringsSigns.DASH_SIGN, onInfoClick = { onInfoClick( InfoBottomSheetContent( @@ -79,7 +80,7 @@ internal class MetricsConverter( ), InfoPointUM( title = resourceReference(R.string.markets_token_details_fully_diluted_valuation), - value = fullyDilutedValuation.formatAmount(), + value = fullyDilutedValuation.formatAmount() ?: StringsSigns.DASH_SIGN, onInfoClick = { onInfoClick( InfoBottomSheetContent( @@ -95,7 +96,7 @@ internal class MetricsConverter( ), InfoPointUM( title = resourceReference(R.string.markets_token_details_circulating_supply), - value = circulatingSupply.formatAmount(crypto = true), + value = circulatingSupply.formatAmount(crypto = true) ?: StringsSigns.DASH_SIGN, onInfoClick = { onInfoClick( InfoBottomSheetContent( @@ -109,7 +110,7 @@ internal class MetricsConverter( ), InfoPointUM( title = resourceReference(R.string.markets_token_details_max_supply), - value = maxSupply.formatMaxSupply(), + value = maxSupply.formatMaxSupply() ?: StringsSigns.DASH_SIGN, onInfoClick = { onInfoClick( InfoBottomSheetContent( @@ -135,7 +136,7 @@ internal class MetricsConverter( val liquidity = getLiquidity(value.volume24h, value.marketCap) persistentListOf( InfoPointUMV2.MarketCap( - capitalizationValue = stringReference(marketCap.formatAmount()), + capitalizationValue = marketCap.formatAmount()?.let(::stringReference), onInfoClick = { onInfoClick( InfoBottomSheetContent( @@ -150,7 +151,7 @@ internal class MetricsConverter( }, ), InfoPointUMV2.TradingVolume( - tradingValue = stringReference(volume24h.formatAmount()), + tradingValue = volume24h.formatAmount()?.let(::stringReference), liquidity = liquidity, trendingVolumeLiquidityType = getTrendingVolumeLiquidityType(liquidity), onInfoClick = { @@ -165,7 +166,9 @@ internal class MetricsConverter( }, ), InfoPointUMV2.MarketPosition( - position = marketRating?.toString() ?: StringsSigns.DASH_SIGN, + position = marketRating?.let { + stringReference(it.toString()) + }, rangeValue = getMarketRatingRangeValue(marketRating), marketRatingType = getMarketRatingType(marketRating), onInfoClick = { @@ -176,12 +179,15 @@ internal class MetricsConverter( ), ) }, + marketRatingChange24H = getMarketRatingChange(marketRatingChange24h), ), InfoPointUMV2.FullyDilutedValuation( - value = resourceReference( - R.string.markets_token_details_valuation_value_in_total, - wrappedList(fullyDilutedValuation.formatAmount()), - ), + value = fullyDilutedValuation?.formatAmount()?.let { formatted -> + resourceReference( + id = R.string.markets_token_details_valuation_value_in_total, + formatArgs = wrappedList(formatted), + ) + }, onInfoClick = { onInfoClick( InfoBottomSheetContent( @@ -196,20 +202,29 @@ internal class MetricsConverter( }, ), InfoPointUMV2.CirculatingSupply( - currentValue = stringReference(circulatingSupply.formatAmount(crypto = true)), - maxValue = maxSupply?.let { supply -> - if (supply > BigDecimal.ZERO) { - stringReference(supply.formatMaxSupply()) - } else { - null + currentValue = circulatingSupply?.formatAmount(true)?.let(::stringReference), + maxValue = when (maxSupply) { + null -> null + BigDecimal.ZERO -> { + resourceReference( + R.string + .markets_token_details_metrics_no_limited, + ) } + else -> maxSupply.formatAmount(true)?.let(::stringReference) }, fillValue = getCirculatingSupplyFillValue(circulatingSupply, maxSupply), onInfoClick = { onInfoClick( InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_max_supply_full), - body = resourceReference(R.string.markets_token_details_total_supply_description), + title = resourceReference( + R.string.markets_token_details_max_supply_and_circulation_full, + ), + body = combinedReference( + resourceReference(R.string.markets_token_details_circulating_supply_description), + stringReference("\n\n"), + resourceReference(R.string.markets_token_details_total_supply_description), + ), ), ) }, @@ -234,7 +249,7 @@ internal class MetricsConverter( ) } - private fun BigDecimal?.formatMaxSupply(): String { + private fun BigDecimal?.formatMaxSupply(): String? { when (this) { null -> return StringsSigns.DASH_SIGN BigDecimal.ZERO -> return StringsSigns.INFINITY_SIGN @@ -243,8 +258,8 @@ internal class MetricsConverter( return this.formatAmount(crypto = true) } - private fun BigDecimal?.formatAmount(crypto: Boolean = false): String { - if (this == null) return StringsSigns.DASH_SIGN + private fun BigDecimal?.formatAmount(crypto: Boolean = false): String? { + if (this == null) return null return if (crypto) { format { @@ -266,9 +281,8 @@ internal class MetricsConverter( } @Suppress("MagicNumber") - private fun getLiquidity(volume24h: BigDecimal?, marketCap: BigDecimal?): Float { - if (volume24h == null || marketCap == null || marketCap == BigDecimal.ZERO) return 0f - + private fun getLiquidity(volume24h: BigDecimal?, marketCap: BigDecimal?): Float? { + if (volume24h == null || marketCap == null || marketCap == BigDecimal.ZERO) return null val ratio = volume24h .divide(marketCap, 6, RoundingMode.HALF_UP) .toFloat() @@ -277,8 +291,9 @@ internal class MetricsConverter( } @Suppress("MagicNumber") - private fun getTrendingVolumeLiquidityType(liquidity: Float): TrendingVolumeLiquidityType { + private fun getTrendingVolumeLiquidityType(liquidity: Float?): TrendingVolumeLiquidityType { return when { + liquidity == null -> TrendingVolumeLiquidityType.UNKNOWN liquidity >= 0.5f -> TrendingVolumeLiquidityType.HIGH liquidity in 0.2f..<0.5f -> TrendingVolumeLiquidityType.MEDIUM else -> TrendingVolumeLiquidityType.LOW @@ -296,8 +311,8 @@ internal class MetricsConverter( } @Suppress("MagicNumber") - private fun getMarketRatingRangeValue(marketRating: Int?): Float { - if (marketRating == null) return 0f + private fun getMarketRatingRangeValue(marketRating: Int?): Float? { + if (marketRating == null) return null return when { marketRating <= 20 -> { @@ -352,4 +367,12 @@ internal class MetricsConverter( return ratio.coerceIn(0f, 1f) } + + private fun getMarketRatingChange(change: Int?): MarketRatingChange24H { + return when { + change == null || change == 0 -> MarketRatingChange24H.NoChanges + change < 0 -> MarketRatingChange24H.Down(-change) + else -> MarketRatingChange24H.Up(change) + } + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/MetricsCards.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/MetricsCards.kt index a5a936a6c8..d485b3bb18 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/MetricsCards.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/MetricsCards.kt @@ -21,14 +21,12 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator import com.tangem.core.ui.ds.progress.TangemLinearProgressIndicatorWithDot import com.tangem.core.ui.ds.row.TangemRowContainer import com.tangem.core.ui.ds.row.TangemRowLayoutId -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.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.LocalIsInDarkTheme import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme @@ -36,6 +34,7 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.features.feed.impl.R import com.tangem.features.feed.ui.components.MetricsCard import com.tangem.features.feed.ui.market.detailed.state.InfoPointUMV2 +import com.tangem.features.feed.ui.market.detailed.state.MarketRatingChange24H import com.tangem.features.feed.ui.market.detailed.state.MarketRatingType import com.tangem.features.feed.ui.market.detailed.state.TrendingVolumeLiquidityType @@ -45,15 +44,7 @@ internal fun MarketCapCard(item: InfoPointUMV2.MarketCap) { modifier = Modifier .heightIn(120.dp) .fillMaxWidth(), - title = { - Text( - text = item.capitalizationValue.resolveReference(), - style = TangemTheme.typography2.headingSemibold20, - color = TangemTheme.colors2.text.neutral.primary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, + title = { MetricValueText(value = item.capitalizationValue) }, content = { InformationTextBlock( text = resourceReference(R.string.markets_token_details_market_capitalization), @@ -69,25 +60,22 @@ internal fun TradingVolumeCard(item: InfoPointUMV2.TradingVolume) { TrendingVolumeLiquidityType.HIGH -> TangemTheme.colors2.markers.backgroundSolidGreen TrendingVolumeLiquidityType.MEDIUM -> TangemTheme.colors2.graphic.status.attention TrendingVolumeLiquidityType.LOW -> TangemTheme.colors2.graphic.status.warning + TrendingVolumeLiquidityType.UNKNOWN -> TangemTheme.colors2.surface.level3 } + val valueColor = metricValueColor(hasData = item.tradingValue != null) + MetricsCard( modifier = Modifier .heightIn(120.dp) .fillMaxWidth(), title = { Row { - Text( - text = item.tradingValue.resolveReference(), - style = TangemTheme.typography2.headingSemibold20, - color = TangemTheme.colors2.text.neutral.primary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + MetricValueText(item.tradingValue) Text( modifier = Modifier.padding(TangemTheme.dimens2.x1), text = stringResourceSafe(R.string.markets_token_details_trading_interval), style = TangemTheme.typography2.captionSemibold11, - color = TangemTheme.colors2.text.neutral.primary, + color = valueColor, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -95,14 +83,17 @@ internal fun TradingVolumeCard(item: InfoPointUMV2.TradingVolume) { }, content = { Column(modifier = Modifier.fillMaxWidth()) { - TangemLinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .height(6.dp), - progress = { item.liquidity }, - color = tradingColor, - backgroundColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant.copy(alpha = .1f), - ) + if (item.liquidity != null) { + TangemLinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(6.dp), + progress = { item.liquidity }, + color = tradingColor, + backgroundColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant + .copy(alpha = .1f), + ) + } SpacerH(12.dp) InformationTextBlock( text = resourceReference(R.string.markets_token_details_trading_volume), @@ -127,37 +118,25 @@ internal fun MarketPositionCard(item: InfoPointUMV2.MarketPosition) { .fillMaxWidth(), title = { Row(verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_big_laurel_left_20), - tint = ratingColor, - contentDescription = null, - ) - - Text( - textAlign = TextAlign.Center, - text = item.position, - color = ratingColor, - style = TangemTheme.typography2.headingSemibold20.copy(letterSpacing = 0.sp), - maxLines = 1, - ) - - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_big_laurel_right_20), - tint = ratingColor, - contentDescription = null, - ) + MarketPositionValue(position = item.position, ratingColor = ratingColor) + if (item.position != null) { + SpacerW(6.dp) + RatingChangeIndicator(change = item.marketRatingChange24H) + } } }, content = { Column(modifier = Modifier.fillMaxWidth()) { - TangemLinearProgressIndicatorWithDot( - modifier = Modifier - .fillMaxWidth() - .height(6.dp), - progress = { item.rangeValue }, - dotColor = TangemTheme.colors2.fill.neutral.primaryInvertedConstant, - backgroundColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant.copy(alpha = .1f), - ) + if (item.rangeValue != null) { + TangemLinearProgressIndicatorWithDot( + modifier = Modifier + .fillMaxWidth() + .height(6.dp), + progress = { item.rangeValue }, + dotColor = TangemTheme.colors2.fill.neutral.primaryInvertedConstant, + backgroundColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant.copy(alpha = .1f), + ) + } SpacerH(12.dp) InformationTextBlock( text = resourceReference(R.string.markets_token_details_market_rating), @@ -177,15 +156,7 @@ internal fun FDVCard(item: InfoPointUMV2.FullyDilutedValuation) { modifier = Modifier .heightIn(120.dp) .fillMaxWidth(), - title = { - Text( - text = item.value.resolveReference(), - style = TangemTheme.typography2.headingSemibold20, - color = TangemTheme.colors2.text.neutral.primary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, + title = { MetricValueText(value = item.value) }, content = { InformationTextBlock( text = resourceReference(R.string.markets_token_details_fully_diluted_valuation), @@ -212,15 +183,11 @@ internal fun CirculatingSupplyCard(item: InfoPointUMV2.CirculatingSupply) { overflow = TextOverflow.Ellipsis, ) - Text( + MetricValueText( + value = item.currentValue, modifier = Modifier .padding(top = 12.dp) .layoutId(TangemRowLayoutId.START_BOTTOM), - text = item.currentValue.resolveReference(), - style = TangemTheme.typography2.headingSemibold20, - color = TangemTheme.colors2.text.neutral.primary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, ) Text( @@ -238,7 +205,7 @@ internal fun CirculatingSupplyCard(item: InfoPointUMV2.CirculatingSupply) { .padding(top = 12.dp) .layoutId(TangemRowLayoutId.END_BOTTOM), text = item.maxValue.resolveReference(), - style = TangemTheme.typography2.headingSemibold20, + style = TangemTheme.typography2.headingSemibold22, color = TangemTheme.colors2.text.neutral.primary, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -253,7 +220,8 @@ internal fun CirculatingSupplyCard(item: InfoPointUMV2.CirculatingSupply) { .fillMaxWidth() .height(6.dp), color = TangemTheme.colors2.graphic.status.accent, - trackColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant.copy(alpha = .1f), + trackColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant + .copy(alpha = .1f), progress = { item.fillValue }, strokeCap = StrokeCap.Round, drawStopIndicator = {}, @@ -265,6 +233,87 @@ internal fun CirculatingSupplyCard(item: InfoPointUMV2.CirculatingSupply) { ) } +// region Private helpers + +@Composable +private fun MetricValueText(value: TextReference?, modifier: Modifier = Modifier) { + Text( + modifier = modifier, + text = value?.resolveReference() ?: stringResourceSafe(R.string.token_market_metrics_no_data), + style = TangemTheme.typography2.headingSemibold22, + color = metricValueColor(hasData = value != null), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) +} + +@Composable +private fun metricValueColor(hasData: Boolean): Color { + return if (hasData) TangemTheme.colors2.text.neutral.primary else TangemTheme.colors2.text.neutral.tertiary +} + +@Composable +private fun MarketPositionValue(position: TextReference?, ratingColor: Color) { + if (position != null) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_big_laurel_left_20), + tint = ratingColor, + contentDescription = null, + ) + Text( + textAlign = TextAlign.Center, + text = position.resolveReference(), + color = ratingColor, + style = TangemTheme.typography2.headingSemibold22.copy(letterSpacing = 0.sp), + maxLines = 1, + ) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_big_laurel_right_20), + tint = ratingColor, + contentDescription = null, + ) + } else { + MetricValueText(value = null) + } +} + +@Composable +private fun RatingChangeIndicator(change: MarketRatingChange24H) { + when (change) { + is MarketRatingChange24H.Up -> RatingChangeContent( + iconRes = R.drawable.ic_arrow_up_8, + iconTint = TangemTheme.colors2.markers.iconGreen, + changeValue = change.changeValue.toString(), + textColor = TangemTheme.colors2.text.status.positive, + ) + is MarketRatingChange24H.Down -> RatingChangeContent( + iconRes = R.drawable.ic_arrow_down_8, + iconTint = TangemTheme.colors2.markers.iconRed, + changeValue = change.changeValue.toString(), + textColor = TangemTheme.colors2.text.status.warning, + ) + MarketRatingChange24H.NoChanges -> Unit + } +} + +@Composable +private fun RatingChangeContent(iconRes: Int, iconTint: Color, changeValue: String, textColor: Color) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + modifier = Modifier.size(TangemTheme.dimens2.x3), + imageVector = ImageVector.vectorResource(id = iconRes), + tint = iconTint, + contentDescription = null, + ) + SpacerW(2.dp) + Text( + text = changeValue, + style = TangemTheme.typography2.captionSemibold12, + color = textColor, + ) + } +} + @Composable private fun MarketRatingType.baseColor(): Color { val isDarkTheme = LocalIsInDarkTheme.current @@ -295,6 +344,8 @@ private fun mapRatingToCardColor(marketRatingType: MarketRatingType): Color { } } +// endregion + private const val GOLD_PLACE_COLOR_NIGHT = 0xFFFBEE76 private const val GOLD_PLACE_COLOR_LIGHT = 0xFFD9B900 private const val SILVER_PLACE_COLOR_NIGHT = 0xFFAABEF7 @@ -353,17 +404,19 @@ private fun MetricsCardsPreview() { MarketPositionCard( item = InfoPointUMV2.MarketPosition( - position = "1", + position = stringReference("1"), rangeValue = 0.02f, marketRatingType = MarketRatingType.GOLD, onInfoClick = {}, + marketRatingChange24H = MarketRatingChange24H.NoChanges, ), ) MarketPositionCard( item = InfoPointUMV2.MarketPosition( - position = "2", + position = stringReference("2"), rangeValue = 0.05f, + marketRatingChange24H = MarketRatingChange24H.Up(1), marketRatingType = MarketRatingType.SILVER, onInfoClick = {}, ), @@ -371,19 +424,21 @@ private fun MetricsCardsPreview() { MarketPositionCard( item = InfoPointUMV2.MarketPosition( - position = "3", - rangeValue = 0.08f, - marketRatingType = MarketRatingType.BRONZE, + position = null, + rangeValue = null, + marketRatingType = MarketRatingType.OTHER, onInfoClick = {}, + marketRatingChange24H = MarketRatingChange24H.NoChanges, ), ) MarketPositionCard( item = InfoPointUMV2.MarketPosition( - position = "42", + position = stringReference("42"), rangeValue = 0.42f, marketRatingType = MarketRatingType.OTHER, onInfoClick = {}, + marketRatingChange24H = MarketRatingChange24H.Down(15), ), ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MetricsUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MetricsUM.kt index 4279c29373..76db464700 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MetricsUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MetricsUM.kt @@ -14,35 +14,36 @@ internal sealed interface InfoPointUMV2 { @Immutable data class MarketCap( - val capitalizationValue: TextReference, + val capitalizationValue: TextReference?, val onInfoClick: () -> Unit, ) : InfoPointUMV2 @Immutable data class TradingVolume( - val tradingValue: TextReference, - val liquidity: Float, + val tradingValue: TextReference?, + val liquidity: Float?, val trendingVolumeLiquidityType: TrendingVolumeLiquidityType, val onInfoClick: () -> Unit, ) : InfoPointUMV2 @Immutable data class MarketPosition( - val position: String, - val rangeValue: Float, + val position: TextReference?, + val rangeValue: Float?, + val marketRatingChange24H: MarketRatingChange24H, val marketRatingType: MarketRatingType, val onInfoClick: () -> Unit, ) : InfoPointUMV2 @Immutable data class FullyDilutedValuation( - val value: TextReference, + val value: TextReference?, val onInfoClick: () -> Unit, ) : InfoPointUMV2 @Immutable data class CirculatingSupply( - val currentValue: TextReference, + val currentValue: TextReference?, val maxValue: TextReference?, val fillValue: Float?, val onInfoClick: () -> Unit, @@ -60,9 +61,18 @@ internal data class MetricsV2UM( } internal enum class TrendingVolumeLiquidityType { - HIGH, MEDIUM, LOW, + HIGH, MEDIUM, LOW, UNKNOWN } internal enum class MarketRatingType { GOLD, SILVER, BRONZE, OTHER +} + +internal sealed interface MarketRatingChange24H { + + data class Up(val changeValue: Int) : MarketRatingChange24H + + data class Down(val changeValue: Int) : MarketRatingChange24H + + data object NoChanges : MarketRatingChange24H } \ No newline at end of file From c76d1de3cfc19a09ffd70e80d9958a56c80440aa Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Mar 2026 16:28:00 +0400 Subject: [PATCH 24/75] Updated on 2026-08-14 --- .../kotlin/com/tangem/tests/FeedbackTest.kt | 4 +- .../tap/common/redux/global/GlobalAction.kt | 12 -- .../common/redux/global/GlobalMiddleware.kt | 36 ----- .../tap/common/redux/global/GlobalReducer.kt | 6 - .../tap/common/redux/global/GlobalState.kt | 1 - .../scanCard/DefaultScanFailsCounter.kt | 37 +++++ .../domain/scanCard/LegacyScanProcessor.kt | 10 +- .../domain/scanCard/UseCaseScanProcessor.kt | 9 +- .../features/scanfails/ScanFailsComponent.kt | 3 +- .../tap/features/scanfails/ScanFailsModel.kt | 14 +- .../scanfails/ScanFailsRequesterProxy.kt | 3 +- .../features/scanfails/di/ScanFailsModule.kt | 6 + .../scanCard/DefaultScanFailsCounterTest.kt | 138 ++++++++++++++++++ .../tangem/domain/card/ScanFailsCounter.kt | 10 ++ .../tangem/domain/card/ScanFailsRequester.kt | 6 +- 15 files changed, 211 insertions(+), 84 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanFailsCounter.kt create mode 100644 app/src/test/kotlin/com/tangem/tap/domain/scanCard/DefaultScanFailsCounterTest.kt create mode 100644 domain/card/src/main/kotlin/com/tangem/domain/card/ScanFailsCounter.kt diff --git a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt index 1009e83a52..6232f3caf0 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt @@ -7,7 +7,7 @@ import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.clickAndWaitFor import com.tangem.common.extensions.clickWithAssertion -import com.tangem.domain.card.ScanFailsRequester +import com.tangem.core.analytics.models.AnalyticsParam import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import com.tangem.scenarios.checkFailedTransactionDialog @@ -179,7 +179,7 @@ class FeedbackTest : BaseTestCase() { runOnUiThread { val requester = store.state.daggerGraphState.scanFailsRequester!! MainScope().launch { - requester.show(ScanFailsRequester.Source.MAIN) + requester.show(AnalyticsParam.ScreensSources.Main) } } } 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 315fe98387..a36bd807b6 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 @@ -1,23 +1,11 @@ package com.tangem.tap.common.redux.global -import com.tangem.common.CompletionResult -import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.scan.ScanResponse import org.rekotlin.Action sealed class GlobalAction : Action { - object ScanFailsCounter { - data class ChooseBehavior( - val result: CompletionResult, - val analyticsSource: AnalyticsParam.ScreensSources, - ) : GlobalAction() - - object Reset : GlobalAction() - object Increment : GlobalAction() - } - data class SaveScanResponse(val scanResponse: ScanResponse) : GlobalAction() data class ChangeAppCurrency(val appCurrency: AppCurrency) : GlobalAction() 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 5bf0f5559a..f2df3df6e9 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 @@ -1,11 +1,6 @@ package com.tangem.tap.common.redux.global -import com.tangem.common.CompletionResult -import com.tangem.common.core.TangemSdkError -import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.card.ScanFailsRequester -import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState @@ -30,43 +25,12 @@ private val globalMiddlewareHandler: Middleware = { _, _ -> } } -@Suppress("LongMethod", "ComplexMethod") private fun handleAction(action: Action) { when (action) { - is GlobalAction.ScanFailsCounter.ChooseBehavior -> { - when (action.result) { - is CompletionResult.Success -> store.dispatch(GlobalAction.ScanFailsCounter.Reset) - is CompletionResult.Failure -> { - handleFailureChooseBehaviour(action.result, action.analyticsSource) - } - } - } is GlobalAction.RestoreAppCurrency -> restoreAppCurrency() } } -private fun handleFailureChooseBehaviour( - result: CompletionResult.Failure, - analyticsSource: AnalyticsParam.ScreensSources, -) { - if (result.error is TangemSdkError.UserCancelled) { - store.dispatch(GlobalAction.ScanFailsCounter.Increment) - if (store.state.globalState.scanCardFailsCounter >= 2) { - val scanFailsSource = when (analyticsSource) { - is AnalyticsParam.ScreensSources.SignIn -> ScanFailsRequester.Source.SIGN_IN - is AnalyticsParam.ScreensSources.Settings -> ScanFailsRequester.Source.SETTINGS - is AnalyticsParam.ScreensSources.Intro -> ScanFailsRequester.Source.INTRO - else -> ScanFailsRequester.Source.MAIN - } - scope.launch { - store.inject(DaggerGraphState::scanFailsRequester).show(scanFailsSource) - } - } - } else { - store.dispatch(GlobalAction.ScanFailsCounter.Reset) - } -} - private fun restoreAppCurrency() { scope.launch { val currency = store.inject(DaggerGraphState::appCurrencyRepository) 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 b381df3054..e46630172e 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 @@ -10,12 +10,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState { val globalState = state.globalState return when (action) { - is GlobalAction.ScanFailsCounter.Increment -> { - globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1) - } - is GlobalAction.ScanFailsCounter.Reset -> { - globalState.copy(scanCardFailsCounter = 0) - } is GlobalAction.SaveScanResponse -> { globalState.copy(scanResponse = action.scanResponse) } 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 3c7906a1e5..01782e2e9a 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 @@ -10,7 +10,6 @@ data class GlobalState( val scanResponse: ScanResponse? = null, val tapWalletManager: TapWalletManager = TapWalletManager(), val appCurrency: AppCurrency = AppCurrency.Default, - val scanCardFailsCounter: Int = 0, val isLastSignWithRing: Boolean = false, ) : StateType diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanFailsCounter.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanFailsCounter.kt new file mode 100644 index 0000000000..cc7df51451 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanFailsCounter.kt @@ -0,0 +1,37 @@ +package com.tangem.tap.domain.scanCard + +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.card.ScanFailsCounter +import com.tangem.domain.card.ScanFailsRequester +import com.tangem.utils.coroutines.AppCoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class DefaultScanFailsCounter @Inject constructor( + private val scanFailsRequester: ScanFailsRequester, + private val appScope: AppCoroutineScope, +) : ScanFailsCounter { + + private var counter: Int = 0 + + override fun reset() { + counter = 0 + } + + override fun onScanFailure(isUserCancelled: Boolean, source: AnalyticsParam.ScreensSources) { + if (isUserCancelled) { + counter++ + if (counter >= THRESHOLD) { + appScope.launch { scanFailsRequester.show(source) } + } + } else { + counter = 0 + } + } + + private companion object { + const val THRESHOLD = 2 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index fd2e386a76..3c0b311ca7 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -19,6 +19,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList import com.tangem.core.ui.message.dialog.Dialogs +import com.tangem.domain.card.ScanFailsCounter import com.tangem.domain.card.common.util.twinsIsTwinned import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.feedback.models.FeedbackEmailType @@ -26,9 +27,7 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.sdk.extensions.localizedDescriptionRes import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.inject -import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.onboarding.OnboardingHelper import com.tangem.tap.mainScope @@ -49,6 +48,7 @@ internal class LegacyScanProcessor @Inject constructor( @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, private val trackingContextProxy: TrackingContextProxy, + private val scanFailsCounter: ScanFailsCounter, ) { suspend fun scan( @@ -89,10 +89,13 @@ internal class LegacyScanProcessor @Inject constructor( ) val analyticsEvent = Basic.CardWasScanned(analyticsSource) - store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result, analyticsSource)) result .doOnFailure { error -> + scanFailsCounter.onScanFailure( + isUserCancelled = error is TangemSdkError.UserCancelled, + source = analyticsSource, + ) onScanFailure( analyticsSource = analyticsSource, error = error, @@ -106,6 +109,7 @@ internal class LegacyScanProcessor @Inject constructor( ) } .doOnSuccess { scanResponse -> + scanFailsCounter.reset() tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse) sendAnalytics(analyticsEvent, scanResponse) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt index f82939019e..d9bbaccdb4 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/UseCaseScanProcessor.kt @@ -9,7 +9,6 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.domain.card.ScanCardException -import com.tangem.domain.card.ScanFailsRequester import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent import com.tangem.tap.common.extensions.dispatchNavigationAction @@ -73,14 +72,8 @@ internal object UseCaseScanProcessor { } private fun showScanFailsDialog(source: AnalyticsParam.ScreensSources) { - val scanFailsSource = when (source) { - is AnalyticsParam.ScreensSources.SignIn -> ScanFailsRequester.Source.SIGN_IN - is AnalyticsParam.ScreensSources.Settings -> ScanFailsRequester.Source.SETTINGS - is AnalyticsParam.ScreensSources.Intro -> ScanFailsRequester.Source.INTRO - else -> ScanFailsRequester.Source.MAIN - } scope.launch { - store.inject(DaggerGraphState::scanFailsRequester).show(scanFailsSource) + store.inject(DaggerGraphState::scanFailsRequester).show(source) } } diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsComponent.kt b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsComponent.kt index ffb2163692..41452daa3c 100644 --- a/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsComponent.kt +++ b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsComponent.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.decompose.model.getOrCreateModel @@ -21,7 +22,7 @@ internal class ScanFailsComponent @AssistedInject constructor( private val model: ScanFailsModel = getOrCreateModel(params) - override suspend fun show(source: ScanFailsRequester.Source): ScanFailsRequester.Result { + override suspend fun show(source: AnalyticsParam.ScreensSources): ScanFailsRequester.Result { model.show(source) return model.waitResult() } diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsModel.kt b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsModel.kt index bbd95fded5..43804e0c66 100644 --- a/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsModel.kt @@ -30,14 +30,13 @@ internal class ScanFailsModel @Inject constructor( val uiState: StateFlow field = MutableStateFlow(ScanFailsUM(onDismiss = ::dismiss)) - fun show(source: ScanFailsRequester.Source) { - val analyticsSource = source.toAnalyticsSource() + fun show(source: AnalyticsParam.ScreensSources) { result.value = null uiState.update { ScanFailsUM( isShown = true, - onHowToScan = { onHowToScan(analyticsSource) }, - onRequestSupport = { onRequestSupport(analyticsSource) }, + onHowToScan = { onHowToScan(source) }, + onRequestSupport = { onRequestSupport(source) }, onDismiss = ::dismiss, ) } @@ -70,11 +69,4 @@ internal class ScanFailsModel @Inject constructor( sendFeedbackEmailUseCase(type = FeedbackEmailType.ScanningProblem) } } - - private fun ScanFailsRequester.Source.toAnalyticsSource(): AnalyticsParam.ScreensSources = when (this) { - ScanFailsRequester.Source.MAIN -> AnalyticsParam.ScreensSources.Main - ScanFailsRequester.Source.SIGN_IN -> AnalyticsParam.ScreensSources.SignIn - ScanFailsRequester.Source.SETTINGS -> AnalyticsParam.ScreensSources.Settings - ScanFailsRequester.Source.INTRO -> AnalyticsParam.ScreensSources.Intro - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsRequesterProxy.kt b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsRequesterProxy.kt index 806d511714..5c4851d54e 100644 --- a/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsRequesterProxy.kt +++ b/app/src/main/java/com/tangem/tap/features/scanfails/ScanFailsRequesterProxy.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.scanfails +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.card.ScanFailsRequester import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull @@ -13,7 +14,7 @@ class ScanFailsRequesterProxy @Inject constructor() : ScanFailsRequester { val componentRequester = MutableStateFlow(null) - override suspend fun show(source: ScanFailsRequester.Source): ScanFailsRequester.Result { + override suspend fun show(source: AnalyticsParam.ScreensSources): ScanFailsRequester.Result { return withTimeout(timeMillis = 1000) { componentRequester.filterNotNull().first() }.show(source) diff --git a/app/src/main/java/com/tangem/tap/features/scanfails/di/ScanFailsModule.kt b/app/src/main/java/com/tangem/tap/features/scanfails/di/ScanFailsModule.kt index 6d637e7594..3785c1dccf 100644 --- a/app/src/main/java/com/tangem/tap/features/scanfails/di/ScanFailsModule.kt +++ b/app/src/main/java/com/tangem/tap/features/scanfails/di/ScanFailsModule.kt @@ -2,7 +2,9 @@ package com.tangem.tap.features.scanfails.di import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.decompose.model.Model +import com.tangem.domain.card.ScanFailsCounter import com.tangem.domain.card.ScanFailsRequester +import com.tangem.tap.domain.scanCard.DefaultScanFailsCounter import com.tangem.tap.features.scanfails.ScanFailsComponent import com.tangem.tap.features.scanfails.ScanFailsModel import com.tangem.tap.features.scanfails.ScanFailsRequesterProxy @@ -29,4 +31,8 @@ internal interface ScanFailsModule { @Binds @Singleton fun bindRequester(impl: ScanFailsRequesterProxy): ScanFailsRequester + + @Binds + @Singleton + fun bindScanFailsCounter(impl: DefaultScanFailsCounter): ScanFailsCounter } \ No newline at end of file diff --git a/app/src/test/kotlin/com/tangem/tap/domain/scanCard/DefaultScanFailsCounterTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/scanCard/DefaultScanFailsCounterTest.kt new file mode 100644 index 0000000000..5f80e2dc56 --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/domain/scanCard/DefaultScanFailsCounterTest.kt @@ -0,0 +1,138 @@ +package com.tangem.tap.domain.scanCard + +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.card.ScanFailsRequester +import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class DefaultScanFailsCounterTest { + + private val scanFailsRequester = mockk() + private val dispatchers = TestingCoroutineDispatcherProvider() + private val appScope = object : AppCoroutineScope { + override val coroutineContext = dispatchers.main + } + + private lateinit var counter: DefaultScanFailsCounter + + @BeforeEach + fun setup() { + clearMocks(scanFailsRequester) + counter = DefaultScanFailsCounter( + scanFailsRequester = scanFailsRequester, + appScope = appScope, + ) + } + + @Test + fun `single user cancellation does not show dialog`() = runTest { + // Arrange + val source = AnalyticsParam.ScreensSources.Main + coEvery { scanFailsRequester.show(source) } returns ScanFailsRequester.Result.Dismissed + + // Act + counter.onScanFailure(isUserCancelled = true, source = source) + + // Assert + coVerify(exactly = 0) { scanFailsRequester.show(source) } + } + + @Test + fun `two consecutive user cancellations show dialog`() = runTest { + // Arrange + val source = AnalyticsParam.ScreensSources.Main + coEvery { scanFailsRequester.show(source) } returns ScanFailsRequester.Result.Dismissed + + // Act + counter.onScanFailure(isUserCancelled = true, source = source) + counter.onScanFailure(isUserCancelled = true, source = source) + + // Assert + coVerify(exactly = 1) { scanFailsRequester.show(source) } + } + + @Test + fun `non-cancelled failure resets counter`() = runTest { + // Arrange + val source = AnalyticsParam.ScreensSources.Main + coEvery { scanFailsRequester.show(source) } returns ScanFailsRequester.Result.Dismissed + + // Act + counter.onScanFailure(isUserCancelled = true, source = source) + counter.onScanFailure(isUserCancelled = false, source = source) + counter.onScanFailure(isUserCancelled = true, source = source) + + // Assert + coVerify(exactly = 0) { scanFailsRequester.show(source) } + } + + @Test + fun `reset clears counter`() = runTest { + // Arrange + val source = AnalyticsParam.ScreensSources.Main + coEvery { scanFailsRequester.show(source) } returns ScanFailsRequester.Result.Dismissed + + // Act + counter.onScanFailure(isUserCancelled = true, source = source) + counter.reset() + counter.onScanFailure(isUserCancelled = true, source = source) + + // Assert + coVerify(exactly = 0) { scanFailsRequester.show(source) } + } + + @Test + fun `after reset two new cancellations show dialog again`() = runTest { + // Arrange + val source = AnalyticsParam.ScreensSources.SignIn + coEvery { scanFailsRequester.show(source) } returns ScanFailsRequester.Result.Dismissed + + // Act + counter.onScanFailure(isUserCancelled = true, source = source) + counter.onScanFailure(isUserCancelled = true, source = source) + counter.reset() + counter.onScanFailure(isUserCancelled = true, source = source) + counter.onScanFailure(isUserCancelled = true, source = source) + + // Assert + coVerify(exactly = 2) { scanFailsRequester.show(source) } + } + + @Test + fun `dialog receives correct source`() = runTest { + // Arrange + val source = AnalyticsParam.ScreensSources.Settings + coEvery { scanFailsRequester.show(source) } returns ScanFailsRequester.Result.Dismissed + + // Act + counter.onScanFailure(isUserCancelled = true, source = source) + counter.onScanFailure(isUserCancelled = true, source = source) + + // Assert + coVerify(exactly = 1) { scanFailsRequester.show(source) } + } + + @Test + fun `third consecutive cancellation also triggers dialog`() = runTest { + // Arrange + val source = AnalyticsParam.ScreensSources.Intro + coEvery { scanFailsRequester.show(source) } returns ScanFailsRequester.Result.Dismissed + + // Act + counter.onScanFailure(isUserCancelled = true, source = source) + counter.onScanFailure(isUserCancelled = true, source = source) + counter.onScanFailure(isUserCancelled = true, source = source) + + // Assert + coVerify(exactly = 2) { scanFailsRequester.show(source) } + } +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanFailsCounter.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanFailsCounter.kt new file mode 100644 index 0000000000..6d1816cc9f --- /dev/null +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanFailsCounter.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.card + +import com.tangem.core.analytics.models.AnalyticsParam + +interface ScanFailsCounter { + + fun reset() + + fun onScanFailure(isUserCancelled: Boolean, source: AnalyticsParam.ScreensSources) +} \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanFailsRequester.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanFailsRequester.kt index 19fbb925c4..f85ebc13d5 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanFailsRequester.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanFailsRequester.kt @@ -1,10 +1,10 @@ package com.tangem.domain.card +import com.tangem.core.analytics.models.AnalyticsParam + interface ScanFailsRequester { - suspend fun show(source: Source): Result - - enum class Source { MAIN, SIGN_IN, SETTINGS, INTRO } + suspend fun show(source: AnalyticsParam.ScreensSources): Result sealed class Result { data object Dismissed : Result() From 7a7af53464df660da3a95c7d6c2c17035fbfea16 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Mar 2026 08:23:48 +0500 Subject: [PATCH 25/75] Updated on 2026-08-14 --- .../core/ui/ds/message/TangemMessageUM.kt | 3 + .../DefaultTangemPayMainBlockComponent.kt | 8 +- .../tangempay/ui/TangemPayMainBlockContent.kt | 429 ++++++++---------- .../ui/TangemPayMainBlockContentLegacy.kt | 355 +++++++++++++++ .../wallet/child/wallet/WalletComponent.kt | 9 +- .../common/preview/WalletScreenPreviewData.kt | 5 +- .../utils/WalletWarningsAnalyticsSender.kt | 2 + .../domain/GetWalletNotificationsFactory.kt | 44 ++ .../state/model/WalletNotificationUM.kt | 42 +- .../wallet/state/model/WalletUM.kt | 7 +- .../transformers/SetTokenListTransformer.kt | 16 +- .../converter/TangemPayMainBlockConverter.kt | 22 +- .../state/utils/WalletLoadingStateFactory.kt | 2 +- .../subscribers/BasicAccountListSubscriber.kt | 4 +- .../presentation/wallet/ui/WalletScreen2.kt | 15 + .../wallet/ui/components/WalletItemBlocks.kt | 24 +- .../ui/components/common/WalletContent.kt | 7 +- 17 files changed, 713 insertions(+), 281 deletions(-) create mode 100644 features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContentLegacy.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageUM.kt index acaabc9b54..2e05ad0f27 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageUM.kt @@ -42,12 +42,14 @@ data class TangemMessageUM( * @param text TextReference for the button label. * @param type TangemButtonType defining the style type of the button. * @param iconRes Drawable resource ID for the icon to be displayed in the button (optional). + * @param isLoading Boolean indicating whether the button should show a loading state. * @param onClick Lambda to be invoked when the button is clicked. */ data class TangemMessageButtonUM( val text: TextReference, val type: TangemButtonType, @DrawableRes val iconRes: Int? = null, + val isLoading: Boolean = false, val onClick: () -> Unit, ) { /** Creates a TangemButtonUM representation of this message button. */ @@ -58,6 +60,7 @@ data class TangemMessageButtonUM( iconRes = iconRes, iconPosition = TangemButtonIconPosition.End, type = type, + isLoading = isLoading, onClick = onClick, ) } \ No newline at end of file diff --git a/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/component/DefaultTangemPayMainBlockComponent.kt b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/component/DefaultTangemPayMainBlockComponent.kt index 716d22e5f7..e2b4e2f930 100644 --- a/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/component/DefaultTangemPayMainBlockComponent.kt +++ b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/component/DefaultTangemPayMainBlockComponent.kt @@ -3,7 +3,9 @@ package com.tangem.features.tangempay.component import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.ui.Modifier import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.features.tangempay.entity.TangemPayMainUM +import com.tangem.features.tangempay.ui.TangemPayMainBlockContent import com.tangem.features.tangempay.ui.TangemPayMainBlockItem import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -26,7 +28,11 @@ internal class DefaultTangemPayMainBlockComponent @AssistedInject constructor( key = TANGEM_PAY_ACCOUNT_CONTENT_TYPE, contentType = TANGEM_PAY_ACCOUNT_CONTENT_TYPE, ) { - TangemPayMainBlockItem(state, isBalanceHidden, modifier) + if (LocalRedesignEnabled.current) { + TangemPayMainBlockContent(state, isBalanceHidden, modifier) + } else { + TangemPayMainBlockItem(state, isBalanceHidden, modifier) + } } } diff --git a/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt index e95dcc262b..54da2fa3ec 100644 --- a/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt +++ b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt @@ -2,123 +2,199 @@ package com.tangem.features.tangempay.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.layoutId import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.CircleShimmer -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.SpacerWMax -import com.tangem.core.ui.components.block.BlockCard -import com.tangem.core.ui.components.inputrow.InputRowImageBase +import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.ds.row.TangemRowContainer +import com.tangem.core.ui.ds.row.TangemRowLayoutId import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.features.tangempay.entity.TangemPayMainUM import com.tangem.features.tangempay.main.impl.R import com.tangem.utils.StringsSigns.DASH_SIGN -private const val DISABLED_ALPHA = 0.6F +private const val DISABLED_ALPHA = 0.5F @Composable -internal fun TangemPayMainBlockItem(state: TangemPayMainUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { +internal fun TangemPayMainBlockContent( + state: TangemPayMainUM, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { when (state) { is TangemPayMainUM.Empty -> Unit - is TangemPayMainUM.Loading -> TangemPayMainLoadingItem(modifier) - is TangemPayMainUM.UnderReview -> TangemPayMainUnderReviewItem(state, modifier) - is TangemPayMainUM.IssuingCard -> TangemPayMainIssuingCardItem(state, modifier) - is TangemPayMainUM.FailedToIssue -> TangemPayMainFailedIssueItem(state, modifier) - is TangemPayMainUM.Content -> TangemPayMainBlockContent(state, isBalanceHidden, modifier) - is TangemPayMainUM.TemporaryUnavailable -> TangemPayMainTempUnavailableItem(modifier) - is TangemPayMainUM.SyncNeeded -> TangemPayMainSyncNeededItem(modifier) - is TangemPayMainUM.ExposedDevice -> TangemPayMainExposedDeviceItem(modifier) + is TangemPayMainUM.Loading -> TangemPayMainLoading(modifier) + is TangemPayMainUM.UnderReview -> TangemPayStateRow( + subtitle = state.subtitle, + modifier = modifier, + onClick = state.onClick, + ) + is TangemPayMainUM.IssuingCard -> TangemPayStateRow( + subtitle = resourceReference(R.string.tangempay_issuing_your_card), + modifier = modifier, + onClick = state.onClick, + ) + is TangemPayMainUM.FailedToIssue -> TangemPayStateRow( + subtitle = resourceReference(R.string.tangempay_failed_to_issue_card), + modifier = modifier, + showError = true, + onClick = state.onClick, + ) + is TangemPayMainUM.Content -> TangemPayMainContent(state, isBalanceHidden, modifier) + is TangemPayMainUM.TemporaryUnavailable -> TangemPayStateRow( + subtitle = stringReference(DASH_SIGN), + modifier = modifier, + isEnabled = false, + ) + is TangemPayMainUM.SyncNeeded -> TangemPayStateRow( + subtitle = resourceReference(R.string.tangempay_payment_account_sync_needed), + modifier = modifier, + isEnabled = false, + ) + is TangemPayMainUM.ExposedDevice -> TangemPayStateRow( + subtitle = resourceReference(R.string.tangem_pay_rooted_device_subtitle), + modifier = modifier, + ) } } @Composable -private fun TangemPayMainBlockContent( - state: TangemPayMainUM.Content, +private fun TangemPayMainContent( + payMainUM: TangemPayMainUM.Content, isBalanceHidden: Boolean, modifier: Modifier = Modifier, ) { - Surface( - modifier = modifier, - shape = TangemTheme.shapes.roundedCornersXMedium, - color = TangemTheme.colors.background.primary, - onClick = state.onClick, + TangemRowContainer( + modifier = modifier + .clip(RoundedCornerShape(size = 18.dp)) + .background(TangemTheme.colors2.surface.level3) + .clickableSingle(onClick = payMainUM.onClick), ) { - Row( + Image( + painter = painterResource(R.drawable.img_visa_36), + contentDescription = null, modifier = Modifier - .fillMaxWidth() - .height(IntrinsicSize.Min) - .padding(horizontal = 12.dp, vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Image( - painter = painterResource(R.drawable.img_visa_36), - contentDescription = null, - modifier = Modifier.size(36.dp), - ) + .layoutId(TangemRowLayoutId.HEAD) + .size(TangemTheme.dimens2.x10) + .padding(end = TangemTheme.dimens2.x2), + ) + Text( + text = stringResourceSafe(R.string.tangempay_payment_account), + color = TangemTheme.colors2.text.neutral.primary, + style = TangemTheme.typography2.bodySemibold16, + modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP), + ) + Text( + text = payMainUM.subtitle.resolveReference(), + color = TangemTheme.colors2.text.neutral.secondary, + style = TangemTheme.typography2.captionSemibold12, + modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM), + ) + TangemPayFiatAmount( + text = payMainUM.balance.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), + isBalanceFlickering = payMainUM.isBalanceFlickering, + isBalanceFromCache = payMainUM.shouldShowOnlyCacheWarning, + modifier = Modifier.layoutId(TangemRowLayoutId.END_TOP), + ) + Text( + text = payMainUM.balanceSubtitle.resolveReference(), + color = TangemTheme.colors2.text.neutral.secondary, + style = TangemTheme.typography2.captionSemibold12, + modifier = Modifier.layoutId(TangemRowLayoutId.END_BOTTOM), + ) + } +} - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - Text( - text = stringResourceSafe(R.string.tangempay_payment_account), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - ) - Text( - text = state.subtitle.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - Column( - modifier = Modifier.fillMaxHeight(), - verticalArrangement = Arrangement.spacedBy(2.dp), - horizontalAlignment = Alignment.End, - ) { - TangemPayFiatAmount( - text = state.balance.resolveReference(), - isBalanceFlickering = state.isBalanceFlickering, - isBalanceFromCache = state.shouldShowOnlyCacheWarning, - isBalanceHidden = isBalanceHidden, - ) - Text( - text = state.balanceSubtitle.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.End, - ) - } +@Composable +private fun TangemPayStateRow( + subtitle: TextReference, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, + showError: Boolean = false, + isEnabled: Boolean = true, +) { + TangemRowContainer( + modifier = modifier + .clip(RoundedCornerShape(size = 18.dp)) + .background(TangemTheme.colors2.surface.level3) + .conditional(onClick != null && isEnabled) { clickableSingle(onClick = requireNotNull(onClick)) }, + ) { + Image( + painter = painterResource(R.drawable.img_visa_36), + contentDescription = null, + modifier = Modifier + .layoutId(TangemRowLayoutId.HEAD) + .size(TangemTheme.dimens2.x10) + .padding(end = TangemTheme.dimens2.x2) + .conditionalCompose(!isEnabled) { + alpha(DISABLED_ALPHA) + }, + ) + Text( + text = stringResourceSafe(R.string.tangempay_payment_account), + color = if (isEnabled) { + TangemTheme.colors2.text.neutral.primary + } else { + TangemTheme.colors2.text.status.disabled + }, + style = TangemTheme.typography2.bodySemibold16, + modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP), + ) + Text( + text = subtitle.resolveReference(), + color = if (isEnabled) { + TangemTheme.colors2.text.neutral.secondary + } else { + TangemTheme.colors2.text.status.disabled + }, + style = TangemTheme.typography2.captionSemibold12, + modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM), + ) + AnimatedVisibility( + visible = showError, + enter = fadeIn(), + exit = fadeOut(), + label = "Error Icon Animation", + modifier = Modifier + .layoutId(TangemRowLayoutId.TAIL) + .size(TangemTheme.dimens2.x4), + ) { + Icon( + painter = painterResource(R.drawable.ic_alert_24), + tint = TangemTheme.colors2.graphic.status.warning, + contentDescription = null, + ) } } } @Composable private fun TangemPayFiatAmount( - text: String, + text: AnnotatedString, isBalanceFlickering: Boolean, isBalanceFromCache: Boolean, - isBalanceHidden: Boolean, modifier: Modifier = Modifier, ) { Row( @@ -134,17 +210,17 @@ private fun TangemPayFiatAmount( Icon( modifier = Modifier.size(12.dp), painter = painterResource(R.drawable.ic_error_sync_24), - tint = TangemTheme.colors.icon.inactive, + tint = TangemTheme.colors2.graphic.neutral.secondary, contentDescription = null, ) } } Text( - text = text.orMaskWithStars(isBalanceHidden), - style = TangemTheme.typography.body2.applyBladeBrush( + text = text, + style = TangemTheme.typography2.bodySemibold16.applyBladeBrush( isEnabled = isBalanceFlickering, - textColor = TangemTheme.colors.text.primary1, + textColor = TangemTheme.colors2.text.neutral.primary, ), textAlign = TextAlign.End, ) @@ -152,189 +228,63 @@ private fun TangemPayFiatAmount( } @Composable -private fun TangemPayMainUnderReviewItem(state: TangemPayMainUM.UnderReview, modifier: Modifier = Modifier) { - BlockCard( +private fun TangemPayMainLoading(modifier: Modifier = Modifier) { + TangemRowContainer( modifier = modifier - .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) - .background(TangemTheme.colors.background.primary), - onClick = state.onClick, + .clip(RoundedCornerShape(size = 18.dp)) + .background(TangemTheme.colors2.surface.level3), ) { - InputRowImageBase( + CircleShimmer( modifier = Modifier - .padding( - all = TangemTheme.dimens.spacing12, - ), - subtitle = resourceReference(R.string.tangempay_payment_account), - caption = state.subtitle, - subtitleColor = TangemTheme.colors.text.primary1, - captionColor = TangemTheme.colors.text.tertiary, - iconResWebp = R.drawable.img_visa_36, + .layoutId(TangemRowLayoutId.HEAD) + .padding(end = TangemTheme.dimens2.x2) + .size(TangemTheme.dimens2.x10), ) - } -} - -@Composable -private fun TangemPayMainTempUnavailableItem(modifier: Modifier = Modifier) { - BlockCard( - modifier = modifier - .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) - .background(TangemTheme.colors.background.primary), - enabled = false, - ) { - InputRowImageBase( - modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), - subtitle = resourceReference(R.string.tangempay_payment_account), - caption = TextReference.Str(DASH_SIGN), - subtitleColor = TangemTheme.colors.text.tertiary, - captionColor = TangemTheme.colors.text.tertiary, - iconResWebp = R.drawable.img_visa_36, - ) - } -} - -@Composable -private fun TangemPayMainIssuingCardItem(state: TangemPayMainUM.IssuingCard, modifier: Modifier = Modifier) { - BlockCard( - modifier = modifier - .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) - .background(TangemTheme.colors.background.primary), - onClick = state.onClick, - ) { - InputRowImageBase( + TextShimmer( + style = TangemTheme.typography2.bodySemibold16, + radius = TangemTheme.dimens2.x25, modifier = Modifier - .padding(all = TangemTheme.dimens.spacing12), - subtitle = resourceReference(R.string.tangempay_payment_account), - caption = resourceReference(R.string.tangempay_issuing_your_card), - subtitleColor = TangemTheme.colors.text.primary1, - captionColor = TangemTheme.colors.text.tertiary, - iconResWebp = R.drawable.img_visa_36, + .layoutId(TangemRowLayoutId.START_TOP) + .width(TangemTheme.dimens2.x25), ) - } -} - -@Composable -private fun TangemPayMainFailedIssueItem(state: TangemPayMainUM.FailedToIssue, modifier: Modifier = Modifier) { - BlockCard( - modifier = modifier - .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) - .background(TangemTheme.colors.background.primary), - onClick = state.onClick, - ) { - InputRowImageBase( + TextShimmer( + style = TangemTheme.typography2.captionSemibold12, + radius = TangemTheme.dimens2.x25, modifier = Modifier - .padding( - all = TangemTheme.dimens.spacing12, - ), - subtitle = TextReference.Res(R.string.tangempay_payment_account), - caption = TextReference.Res(R.string.tangempay_failed_to_issue_card), - subtitleColor = TangemTheme.colors.text.primary1, - captionColor = TangemTheme.colors.text.tertiary, - iconResWebp = com.tangem.core.ui.R.drawable.img_visa_36, - iconEndRes = R.drawable.ic_alert_24, - endIconTint = TangemTheme.colors.icon.warning, + .layoutId(TangemRowLayoutId.START_BOTTOM) + .width(TangemTheme.dimens2.x11), ) - } -} - -@Composable -private fun TangemPayMainSyncNeededItem(modifier: Modifier = Modifier) { - BlockCard( - modifier = modifier - .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) - .background(TangemTheme.colors.background.primary), - enabled = false, - ) { - InputRowImageBase( - modifier = Modifier.padding( - all = TangemTheme.dimens.spacing12, - ), - subtitle = resourceReference(R.string.tangempay_payment_account), - caption = resourceReference(R.string.tangempay_payment_account_sync_needed), - subtitleColor = TangemTheme.colors.text.tertiary, - captionColor = TangemTheme.colors.text.tertiary, - iconResWebp = R.drawable.img_visa_36, - ) - } -} - -@Composable -private fun TangemPayMainExposedDeviceItem(modifier: Modifier = Modifier) { - BlockCard( - modifier = modifier - .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) - .background(TangemTheme.colors.background.primary) - .alpha(DISABLED_ALPHA), - enabled = false, - ) { - InputRowImageBase( + TextShimmer( + style = TangemTheme.typography2.bodyRegular16, + radius = TangemTheme.dimens2.x25, modifier = Modifier - .padding( - all = TangemTheme.dimens.spacing12, - ), - subtitle = resourceReference(R.string.tangempay_payment_account), - caption = resourceReference(R.string.tangem_pay_rooted_device_subtitle), - subtitleColor = TangemTheme.colors.text.primary1, - captionColor = TangemTheme.colors.text.tertiary, - iconResWebp = R.drawable.img_visa_36, - endIconTint = TangemTheme.colors.icon.warning, + .layoutId(TangemRowLayoutId.END_TOP) + .width(TangemTheme.dimens2.x20), + ) + TextShimmer( + style = TangemTheme.typography2.bodyRegular16, + radius = TangemTheme.dimens2.x25, + modifier = Modifier + .layoutId(TangemRowLayoutId.END_BOTTOM) + .width(TangemTheme.dimens2.x11), ) } } +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun TangemPayMainLoadingItem(modifier: Modifier = Modifier) { - Row( - modifier = modifier - .fillMaxWidth() - .background(color = TangemTheme.colors.background.primary, shape = TangemTheme.shapes.roundedCornersXMedium) - .padding(horizontal = 12.dp, vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - CircleShimmer(modifier = Modifier.size(36.dp)) - Column( - modifier = Modifier.padding(start = 12.dp), - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - RectangleShimmer( - modifier = Modifier - .padding(vertical = 4.dp) - .sizeIn(minWidth = 70.dp, minHeight = 12.dp), - ) - RectangleShimmer( - modifier = Modifier - .padding(vertical = 2.dp) - .sizeIn(minWidth = 52.dp, minHeight = 12.dp), - ) - } - SpacerWMax() - Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { - RectangleShimmer( - modifier = Modifier - .padding(vertical = 4.dp) - .sizeIn(minWidth = 40.dp, minHeight = 12.dp), - ) - RectangleShimmer( - modifier = Modifier - .padding(vertical = 2.dp) - .sizeIn(minWidth = 40.dp, minHeight = 12.dp), - ) - } - } -} - -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Preview -@Composable -private fun TangemPayMainItemsPreview( - @PreviewParameter(TangemPayMainUMPreviewParameterProvider::class) +private fun TangemPayMainBlockContent_Preview( + @PreviewParameter(TangemPayMainBlockContentPreviewParameterProvider::class) state: TangemPayMainUM, ) { - TangemThemePreview { - TangemPayMainBlockItem(state = state, isBalanceHidden = false) + TangemThemePreviewRedesign { + TangemPayMainBlockContent(state = state, isBalanceHidden = false) } } -private class TangemPayMainUMPreviewParameterProvider : CollectionPreviewParameterProvider( +private class TangemPayMainBlockContentPreviewParameterProvider : CollectionPreviewParameterProvider( collection = listOf( TangemPayMainUM.Loading, TangemPayMainUM.SyncNeeded, @@ -352,4 +302,5 @@ private class TangemPayMainUMPreviewParameterProvider : CollectionPreviewParamet shouldShowOnlyCacheWarning = true, ), ), -) \ No newline at end of file +) +// endregion \ No newline at end of file diff --git a/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContentLegacy.kt b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContentLegacy.kt new file mode 100644 index 0000000000..e95dcc262b --- /dev/null +++ b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContentLegacy.kt @@ -0,0 +1,355 @@ +package com.tangem.features.tangempay.ui + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.inputrow.InputRowImageBase +import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.tangempay.entity.TangemPayMainUM +import com.tangem.features.tangempay.main.impl.R +import com.tangem.utils.StringsSigns.DASH_SIGN + +private const val DISABLED_ALPHA = 0.6F + +@Composable +internal fun TangemPayMainBlockItem(state: TangemPayMainUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { + when (state) { + is TangemPayMainUM.Empty -> Unit + is TangemPayMainUM.Loading -> TangemPayMainLoadingItem(modifier) + is TangemPayMainUM.UnderReview -> TangemPayMainUnderReviewItem(state, modifier) + is TangemPayMainUM.IssuingCard -> TangemPayMainIssuingCardItem(state, modifier) + is TangemPayMainUM.FailedToIssue -> TangemPayMainFailedIssueItem(state, modifier) + is TangemPayMainUM.Content -> TangemPayMainBlockContent(state, isBalanceHidden, modifier) + is TangemPayMainUM.TemporaryUnavailable -> TangemPayMainTempUnavailableItem(modifier) + is TangemPayMainUM.SyncNeeded -> TangemPayMainSyncNeededItem(modifier) + is TangemPayMainUM.ExposedDevice -> TangemPayMainExposedDeviceItem(modifier) + } +} + +@Composable +private fun TangemPayMainBlockContent( + state: TangemPayMainUM.Content, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier, + shape = TangemTheme.shapes.roundedCornersXMedium, + color = TangemTheme.colors.background.primary, + onClick = state.onClick, + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) + .padding(horizontal = 12.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Image( + painter = painterResource(R.drawable.img_visa_36), + contentDescription = null, + modifier = Modifier.size(36.dp), + ) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = stringResourceSafe(R.string.tangempay_payment_account), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = state.subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + Column( + modifier = Modifier.fillMaxHeight(), + verticalArrangement = Arrangement.spacedBy(2.dp), + horizontalAlignment = Alignment.End, + ) { + TangemPayFiatAmount( + text = state.balance.resolveReference(), + isBalanceFlickering = state.isBalanceFlickering, + isBalanceFromCache = state.shouldShowOnlyCacheWarning, + isBalanceHidden = isBalanceHidden, + ) + Text( + text = state.balanceSubtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.End, + ) + } + } + } +} + +@Composable +private fun TangemPayFiatAmount( + text: String, + isBalanceFlickering: Boolean, + isBalanceFromCache: Boolean, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + AnimatedVisibility(isBalanceFromCache) { + Row( + modifier = Modifier.padding(horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + modifier = Modifier.size(12.dp), + painter = painterResource(R.drawable.ic_error_sync_24), + tint = TangemTheme.colors.icon.inactive, + contentDescription = null, + ) + } + } + + Text( + text = text.orMaskWithStars(isBalanceHidden), + style = TangemTheme.typography.body2.applyBladeBrush( + isEnabled = isBalanceFlickering, + textColor = TangemTheme.colors.text.primary1, + ), + textAlign = TextAlign.End, + ) + } +} + +@Composable +private fun TangemPayMainUnderReviewItem(state: TangemPayMainUM.UnderReview, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + onClick = state.onClick, + ) { + InputRowImageBase( + modifier = Modifier + .padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = state.subtitle, + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + ) + } +} + +@Composable +private fun TangemPayMainTempUnavailableItem(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + enabled = false, + ) { + InputRowImageBase( + modifier = Modifier.padding(all = TangemTheme.dimens.spacing12), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = TextReference.Str(DASH_SIGN), + subtitleColor = TangemTheme.colors.text.tertiary, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + ) + } +} + +@Composable +private fun TangemPayMainIssuingCardItem(state: TangemPayMainUM.IssuingCard, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + onClick = state.onClick, + ) { + InputRowImageBase( + modifier = Modifier + .padding(all = TangemTheme.dimens.spacing12), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = resourceReference(R.string.tangempay_issuing_your_card), + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + ) + } +} + +@Composable +private fun TangemPayMainFailedIssueItem(state: TangemPayMainUM.FailedToIssue, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + onClick = state.onClick, + ) { + InputRowImageBase( + modifier = Modifier + .padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = TextReference.Res(R.string.tangempay_payment_account), + caption = TextReference.Res(R.string.tangempay_failed_to_issue_card), + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = com.tangem.core.ui.R.drawable.img_visa_36, + iconEndRes = R.drawable.ic_alert_24, + endIconTint = TangemTheme.colors.icon.warning, + ) + } +} + +@Composable +private fun TangemPayMainSyncNeededItem(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary), + enabled = false, + ) { + InputRowImageBase( + modifier = Modifier.padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = resourceReference(R.string.tangempay_payment_account_sync_needed), + subtitleColor = TangemTheme.colors.text.tertiary, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + ) + } +} + +@Composable +private fun TangemPayMainExposedDeviceItem(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier + .clip(RoundedCornerShape(size = TangemTheme.dimens.radius14)) + .background(TangemTheme.colors.background.primary) + .alpha(DISABLED_ALPHA), + enabled = false, + ) { + InputRowImageBase( + modifier = Modifier + .padding( + all = TangemTheme.dimens.spacing12, + ), + subtitle = resourceReference(R.string.tangempay_payment_account), + caption = resourceReference(R.string.tangem_pay_rooted_device_subtitle), + subtitleColor = TangemTheme.colors.text.primary1, + captionColor = TangemTheme.colors.text.tertiary, + iconResWebp = R.drawable.img_visa_36, + endIconTint = TangemTheme.colors.icon.warning, + ) + } +} + +@Composable +private fun TangemPayMainLoadingItem(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .background(color = TangemTheme.colors.background.primary, shape = TangemTheme.shapes.roundedCornersXMedium) + .padding(horizontal = 12.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CircleShimmer(modifier = Modifier.size(36.dp)) + Column( + modifier = Modifier.padding(start = 12.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + RectangleShimmer( + modifier = Modifier + .padding(vertical = 4.dp) + .sizeIn(minWidth = 70.dp, minHeight = 12.dp), + ) + RectangleShimmer( + modifier = Modifier + .padding(vertical = 2.dp) + .sizeIn(minWidth = 52.dp, minHeight = 12.dp), + ) + } + SpacerWMax() + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + RectangleShimmer( + modifier = Modifier + .padding(vertical = 4.dp) + .sizeIn(minWidth = 40.dp, minHeight = 12.dp), + ) + RectangleShimmer( + modifier = Modifier + .padding(vertical = 2.dp) + .sizeIn(minWidth = 40.dp, minHeight = 12.dp), + ) + } + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview +@Composable +private fun TangemPayMainItemsPreview( + @PreviewParameter(TangemPayMainUMPreviewParameterProvider::class) + state: TangemPayMainUM, +) { + TangemThemePreview { + TangemPayMainBlockItem(state = state, isBalanceHidden = false) + } +} + +private class TangemPayMainUMPreviewParameterProvider : CollectionPreviewParameterProvider( + collection = listOf( + TangemPayMainUM.Loading, + TangemPayMainUM.SyncNeeded, + TangemPayMainUM.TemporaryUnavailable, + TangemPayMainUM.ExposedDevice, + TangemPayMainUM.FailedToIssue(onClick = {}), + TangemPayMainUM.UnderReview(subtitle = resourceReference(R.string.tangempay_kyc_in_progress), onClick = {}), + TangemPayMainUM.IssuingCard(onClick = {}), + TangemPayMainUM.Content( + subtitle = TextReference.Str("*1234"), + isBalanceFlickering = true, + balance = TextReference.Str("$ 101.56"), + balanceSubtitle = TextReference.Str("USDC"), + onClick = {}, + shouldShowOnlyCacheWarning = true, + ), + ), +) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index 22f857a5f1..a40b3e413a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -16,12 +16,11 @@ import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.DesignFeatureToggles -import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles -import com.tangem.features.promobanners.api.PromoBannersBlockComponent import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableDialogComponent +import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.tokens.model.details.TokenAction import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent import com.tangem.feature.wallet.child.tokenActions.TokenActionsComponent @@ -32,13 +31,14 @@ import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen2 import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejectedComponent import com.tangem.feature.walletsettings.component.RenameWalletComponent -import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.feed.entry.components.FeedEntryComponent +import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles +import com.tangem.features.promobanners.api.PromoBannersBlockComponent import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.api.PushNotificationsParams -import com.tangem.features.tangempay.component.TangemPayMainBlockComponent import com.tangem.features.send.v2.api.NetworkSelectionComponent +import com.tangem.features.tangempay.component.TangemPayMainBlockComponent import com.tangem.features.tokenreceive.TokenReceiveComponent import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent import dagger.assisted.Assisted @@ -231,6 +231,7 @@ internal class WalletComponent @AssistedInject constructor( if (designFeatureToggles.isRedesignEnabled) { WalletScreen2( state = uiState, + tangemPayComponent = tangemPayMainBlockComponent, bottomSheetContent = { BottomSheetContent( bottomSheetState = bottomSheetState, 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 16187090af..44b3aa8329 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 @@ -17,6 +17,7 @@ import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy.top import com.tangem.feature.wallet.presentation.preview.WalletBalancePreview import com.tangem.feature.wallet.presentation.preview.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.features.tangempay.entity.TangemPayMainUM import kotlinx.collections.immutable.persistentListOf internal object WalletScreenPreviewData { @@ -167,7 +168,7 @@ internal object WalletScreenPreviewData { isFlickering = false, onItemClick = {}, ), - tangemPayState = TangemPayState.Loading, + tangemPayMainUM = TangemPayMainUM.Loading, ) private val walletEmpty = WalletUM.Content( @@ -182,7 +183,7 @@ internal object WalletScreenPreviewData { notificationsCarousel = persistentListOf(), tokensListUM = WalletTokensListUM.Empty(onEmptyClick = {}), nftState = WalletNFTItemUM.Hidden, - tangemPayState = TangemPayState.Empty, + tangemPayMainUM = TangemPayMainUM.Empty, ) private val walletAccountDefault = walletDefault.copy( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 32b3a72da1..96f7c46639 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -168,6 +168,8 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( WalletNotificationUM.SomeNetworksUnreachable, is WalletNotificationUM.UsedOutdatedData, is WalletNotificationUM.CloreMigration, + is WalletNotificationUM.TangemPayRefreshNeeded, + WalletNotificationUM.TangemPayUnreachable, -> null } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt index 51f3323547..8ce6b1a9c0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.domain import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.ui.ds.message.TangemMessageEffect +import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver @@ -10,12 +11,15 @@ import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM import com.tangem.hot.sdk.model.HotWalletId @@ -56,6 +60,10 @@ internal class GetWalletNotificationsFactory @Inject constructor( val totalFiatBalance = accountList.totalFiatBalance val flattenCurrencies = accountList.flattenCurrencies() + val paymentAccountStatus = accountList.accountStatuses + .filterIsInstance() + .firstOrNull() + buildList { addUsedOutdatedDataNotification(totalFiatBalance) @@ -82,6 +90,14 @@ internal class GetWalletNotificationsFactory @Inject constructor( isNeedToBackup = isNeedToBackup, clickIntents = clickIntents, ) + + if (paymentAccountStatus != null) { + addTangemPayWarnings( + status = paymentAccountStatus, + userWallet = userWallet, + walletClickIntents = clickIntents, + ) + } }.sortedBy { it.type.ordinal }.toImmutableList() } } @@ -200,6 +216,34 @@ internal class GetWalletNotificationsFactory @Inject constructor( ) } + private fun MutableList.addTangemPayWarnings( + status: AccountStatus.Payment, + userWallet: UserWallet, + walletClickIntents: WalletClickIntents, + ) { + val notification = when (status.value) { + is PaymentAccountStatusValue.Error.NotSynced -> WalletNotificationUM.TangemPayRefreshNeeded( + buttonText = when (userWallet) { + is UserWallet.Cold -> resourceReference(id = R.string.home_button_scan) + is UserWallet.Hot -> resourceReference(id = R.string.tangempay_sync_needed_restore_access) + }, + onRefreshClick = { walletClickIntents.onRefreshPayToken(userWallet) }, + shouldShowProgress = false, + ) + is PaymentAccountStatusValue.NotCreated -> null // TODO(Main redesign) + is PaymentAccountStatusValue.Error.Unavailable -> WalletNotificationUM.TangemPayUnreachable + is PaymentAccountStatusValue.Error.CardIssueFailed, + is PaymentAccountStatusValue.Error.ExposedDevice, + is PaymentAccountStatusValue.IssuingCard, + is PaymentAccountStatusValue.Loaded, + is PaymentAccountStatusValue.Loading, + is PaymentAccountStatusValue.Locked, + is PaymentAccountStatusValue.UnderReview, + -> null + } + notification?.let(::add) + } + private fun MutableList.addNoAccountWarning(cryptoCurrencyStatus: CryptoCurrencyStatus?) { val noAccountStatus = cryptoCurrencyStatus?.value as? CryptoCurrencyStatus.NoAccount if (noAccountStatus != null) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt index f3644ec928..fdefd1e7bf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt @@ -7,10 +7,7 @@ import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds.message.TangemMessageButtonUM import com.tangem.core.ui.ds.message.TangemMessageEffect import com.tangem.core.ui.ds.message.TangemMessageUM -import com.tangem.core.ui.extensions.pluralReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.impl.R import kotlinx.collections.immutable.persistentListOf @@ -297,6 +294,43 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t ), type = WalletNotificationType.Warning, ) + + data class TangemPayRefreshNeeded( + private val onRefreshClick: () -> Unit, + private val buttonText: TextReference, + private val shouldShowProgress: Boolean, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "TangemPayRefreshNeeded", + title = resourceReference(id = R.string.tangempay_payment_account_sync_needed), + subtitle = resourceReference(id = R.string.tangempay_use_tangem_device_to_restore_payment_account), + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = buttonText, + iconRes = R.drawable.ic_tangem_24, + onClick = onRefreshClick, + type = TangemButtonType.Primary, + isLoading = shouldShowProgress, + ), + ), + messageEffect = TangemMessageEffect.Card, + isCentered = true, + ), + type = WalletNotificationType.Warning, + ) + + data object TangemPayUnreachable : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "TangemPayUnreachable", + title = resourceReference(id = R.string.tangempay_temporarily_unavailable), + subtitle = resourceReference(id = R.string.tangempay_service_unreachable_try_later), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + ), + type = WalletNotificationType.Warning, + ) // endregion // region Promo diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt index a225e271a3..fd25cf396e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.features.tangempay.entity.TangemPayMainUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -23,7 +24,7 @@ internal sealed interface WalletUM { val type: WalletType - val tangemPayState: TangemPayState + val tangemPayMainUM: TangemPayMainUM data class Content( override val pullToRefreshConfig: PullToRefreshConfig, @@ -34,7 +35,7 @@ internal sealed interface WalletUM { override val tokensListUM: WalletTokensListUM, override val nftState: WalletNFTItemUM, override val type: WalletType, - override val tangemPayState: TangemPayState, + override val tangemPayMainUM: TangemPayMainUM, ) : WalletUM data class Locked( @@ -47,6 +48,6 @@ internal sealed interface WalletUM { override val pullToRefreshConfig = PullToRefreshConfig(false, {}) override val tokensListUM: WalletTokensListUM = WalletTokensListUM.Locked override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden - override val tangemPayState: TangemPayState = TangemPayState.Empty + override val tangemPayMainUM: TangemPayMainUM = TangemPayMainUM.Empty } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 4c3d0689dc..27046856a0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -7,16 +7,13 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.StakingAvailability import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.* -import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletBalanceUMTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter -import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TangemPayMainBlockConverter -import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter -import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.WalletTokensListUMConverter +import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.* import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons -import com.tangem.utils.logging.TangemLogger import com.tangem.features.tangempay.entity.TangemPayMainUM +import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal +@Suppress("LongParameterList") internal class SetTokenListTransformer( private val params: TokenConverterParams, private val userWallet: UserWallet, @@ -26,10 +23,14 @@ internal class SetTokenListTransformer( private val stakingAvailabilityMap: Map = emptyMap(), private val shouldShowMainPromo: Boolean, private val isAccountsModeEnabled: Boolean, + private val isRedesignEnabled: Boolean, ) : WalletStateTransformer(userWallet.walletId) { private val tangemPayConverter by lazy { - TangemPayMainBlockConverter(tangemPayClickIntents = clickIntents) + TangemPayMainBlockConverter( + tangemPayClickIntents = clickIntents, + isRedesignEnabled = isRedesignEnabled, + ) } override fun transform(prevState: WalletState): WalletState { @@ -59,6 +60,7 @@ internal class SetTokenListTransformer( is WalletUM.Content -> { walletUM.copy( walletsBalanceUM = walletUM.walletsBalanceUM.toLoadedState2(), + tangemPayMainUM = walletUM.tangemPayMainUM.toLoadedState(), tokensListUM = toLoadedState(), buttons = walletUM.enableButtons(), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt index 41b74bc551..1ce25e26ac 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt @@ -1,10 +1,13 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter +import androidx.compose.ui.text.SpanStyle import com.tangem.common.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.formatStyled +import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.models.StatusSource import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.PaymentAccountStatusValue @@ -21,6 +24,7 @@ private const val POLYGON_CHAIN_ID = 137 internal class TangemPayMainBlockConverter( private val tangemPayClickIntents: TangemPayIntents, + private val isRedesignEnabled: Boolean, ) : Converter { @Suppress("LongMethod", "CyclomaticComplexMethod") override fun convert(value: AccountStatus.Payment): TangemPayMainUM { @@ -102,9 +106,21 @@ internal class TangemPayMainBlockConverter( private fun getBalanceText(currencyCode: String, balance: BigDecimal): TextReference { val currency = Currency.getInstance(currencyCode) - val formattedBalance = balance.format { - fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol) + val formattedBalance = if (isRedesignEnabled) { + balance.formatStyled { + fiat( + fiatCurrencyCode = currency.currencyCode, + fiatCurrencySymbol = currency.symbol, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ) + } + } else { + stringReference( + balance.format { + fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol) + }, + ) } - return stringReference(formattedBalance) + return formattedBalance } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index cb033c72d5..1b1be5d304 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -69,7 +69,7 @@ internal class WalletLoadingStateFactory( is UserWallet.Cold -> WalletType.Cold is UserWallet.Hot -> WalletType.Hot }, - tangemPayState = TangemPayState.Empty, + tangemPayMainUM = TangemPayMainUM.Empty, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index 463dd73e6e..54b8d8deb9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -17,9 +17,9 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListErrorTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged -import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal /** @@ -103,6 +103,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, isAccountsModeEnabled = isAccountMode, + isRedesignEnabled = true, ), ) } @@ -165,6 +166,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, isAccountsModeEnabled = false, + isRedesignEnabled = false, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt index 27ff0395e6..cfa0ea99a6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager @@ -68,6 +69,8 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.common.Wallet import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletPagerIndicator import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletTopBar import com.tangem.feature.wallet.presentation.wallet.ui.utils.lazyListStateMapSaver +import com.tangem.features.tangempay.component.TangemPayMainBlockComponent +import com.tangem.features.tangempay.entity.TangemPayMainUM import kotlinx.coroutines.launch import kotlin.math.abs @@ -77,6 +80,7 @@ private const val MARKET_HINT_THRESHOLD = 0.5f @Composable internal fun WalletScreen2( state: WalletScreenState, + tangemPayComponent: TangemPayMainBlockComponent, bottomSheetContent: @Composable (() -> Unit), bottomSheetHeaderHeightProvider: () -> Dp, onBottomSheetStateChange: (BottomSheetState) -> Unit, @@ -104,6 +108,7 @@ internal fun WalletScreen2( WalletContent2( state = state, walletsPagerState = walletsPagerState, + tangemPayComponent = tangemPayComponent, behavior = behavior, bottomSheetContent = bottomSheetContent, bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, @@ -128,6 +133,7 @@ internal fun WalletScreen2( private fun WalletContent2( state: WalletScreenState, walletsPagerState: PagerState, + tangemPayComponent: TangemPayMainBlockComponent, behavior: TangemCollapsingAppBarBehavior, bottomSheetHeaderHeightProvider: () -> Dp, onBottomSheetStateChange: (BottomSheetState) -> Unit, @@ -257,6 +263,7 @@ private fun WalletContent2( currentWallet = currentWallet, listState = listState, isBalanceHidden = state.isHidingMode, + tangemPayComponent = tangemPayComponent, contentPadding = contentPadding, modifier = Modifier .fillMaxSize() @@ -565,6 +572,14 @@ private fun WalletScreen2_Preview(@PreviewParameter(WalletScreen2PreviewProvider TangemThemePreviewRedesign { WalletScreen2( state = data, + tangemPayComponent = object : TangemPayMainBlockComponent { + override fun LazyListScope.tangemPayMainContent( + state: TangemPayMainUM, + isBalanceHidden: Boolean, + modifier: Modifier, + ) { + } + }, bottomSheetContent = { Text("Markets Content") }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt index 56506408ef..bc67c35655 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt @@ -3,9 +3,9 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.ui.Modifier import com.tangem.core.ui.ds.button.TangemButton -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM -import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPayMainScreenBlock +import com.tangem.features.tangempay.component.TangemPayMainBlockComponent +import com.tangem.features.tangempay.entity.TangemPayMainUM internal fun LazyListScope.nftCollections2(state: WalletUM, itemModifier: Modifier) { (state as? WalletUM.Content)?.let { content -> @@ -33,17 +33,13 @@ internal fun LazyListScope.organizeTokens2(state: WalletUM, itemModifier: Modifi } } -internal fun LazyListScope.tangemPay(walletUM: WalletUM, isBalanceHiding: Boolean, modifier: Modifier = Modifier) { - if (walletUM is WalletState.MultiCurrency) { - item( - key = "TangemPayMainScreenBlock", - contentType = walletUM.tangemPayState::class.java, - ) { - TangemPayMainScreenBlock( - state = walletUM.tangemPayState, - isBalanceHidden = isBalanceHiding, - modifier = modifier, - ) - } +internal fun LazyListScope.tangemPay( + tangemPayComponent: TangemPayMainBlockComponent, + tangemPayUM: TangemPayMainUM, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + with(tangemPayComponent) { + tangemPayMainContent(modifier = modifier, state = tangemPayUM, isBalanceHidden = isBalanceHidden) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt index 6217c6f14f..5a17a286bc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt @@ -23,6 +23,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency import com.tangem.feature.wallet.presentation.wallet.ui.components.nftCollections2 import com.tangem.feature.wallet.presentation.wallet.ui.components.organizeTokens2 import com.tangem.feature.wallet.presentation.wallet.ui.components.tangemPay +import com.tangem.features.tangempay.component.TangemPayMainBlockComponent import kotlinx.collections.immutable.toPersistentList @Composable @@ -30,6 +31,7 @@ internal fun WalletListContent( currentWallet: WalletUM, isBalanceHidden: Boolean, listState: LazyListState, + tangemPayComponent: TangemPayMainBlockComponent, contentPadding: PaddingValues, modifier: Modifier = Modifier, ) { @@ -57,8 +59,9 @@ internal fun WalletListContent( ) tangemPay( - walletUM = currentWallet, - isBalanceHiding = isBalanceHidden, + tangemPayComponent = tangemPayComponent, + tangemPayUM = currentWallet.tangemPayMainUM, + isBalanceHidden = isBalanceHidden, modifier = itemModifier, ) From 9ee1319e40358248f32a516380579062181a0738 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Mar 2026 06:24:04 +0300 Subject: [PATCH 26/75] Updated on 2026-08-14 --- .../portfolio/add/AddToPortfolioManager.kt | 1 + .../feature/swap/DefaultSwapComponent.kt | 50 +- .../choosetoken/api/ChooseTokenComponent.kt | 82 ++++ .../choosetoken/api/SettingContextUseCase.kt | 40 ++ .../impl/DefaultChooseTokenBridge.kt | 46 ++ .../impl/DefaultChooseTokenComponent.kt | 72 +++ .../choosetoken/impl/di/ChooseTokenModule.kt | 39 ++ .../impl/model/ChooseTokenModel.kt | 289 +++++++++++ .../swap/converters/TokensDataConverter.kt | 92 ++-- .../tangem/feature/swap/model/SwapModel.kt | 459 +++++------------- .../swap/models/SwapSelectTokenStateHolder.kt | 37 +- .../feature/swap/models/SwapStateHolder.kt | 1 - .../tangem/feature/swap/models/UiActions.kt | 2 - ...ager.kt => MarketsListBatchFlowManager.kt} | 35 +- .../models/market/state/SwapMarketState.kt | 15 +- .../tangem/feature/swap/ui/StateBuilder.kt | 34 -- .../feature/swap/ui/SwapSelectTokenScreen.kt | 178 +------ .../preview/SwapSelectTokenPreviewProvider.kt | 27 -- 18 files changed, 786 insertions(+), 713 deletions(-) create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/SettingContextUseCase.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/di/ChooseTokenModule.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt rename features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/{SwapMarketsListBatchFlowManager.kt => MarketsListBatchFlowManager.kt} (88%) diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioManager.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioManager.kt index 8d4e35e9ce..50b323e258 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioManager.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioManager.kt @@ -10,6 +10,7 @@ import kotlinx.serialization.Serializable interface AddToPortfolioManager { + // todo swap make updatable val token: TokenMarketParams val analyticsParams: AnalyticsParams? val portfolioFetcher: PortfolioFetcher diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index 6f5e860827..7035f014b0 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.background import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate @@ -14,23 +13,21 @@ import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.essenty.lifecycle.subscribe import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.R -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent import com.tangem.feature.swap.model.SwapModel -import com.tangem.feature.swap.models.AddToPortfolioRoute import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.ui.SwapScreen -import com.tangem.feature.swap.ui.SwapSelectTokenScreen import com.tangem.feature.swap.ui.SwapSuccessScreen import com.tangem.features.approval.api.GiveApprovalComponent -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.swap.SwapComponent import com.tangem.utils.extensions.isZero @@ -45,19 +42,19 @@ internal class DefaultSwapComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: SwapComponent.Params, private val swapFeeSelectorBlockComponentFactory: SwapFeeSelectorBlockComponent.Factory, - private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, private val giveApprovalComponentFactory: GiveApprovalComponent.Factory, + private val chooseTokenComponentFactory: ChooseTokenComponent.Factory, ) : SwapComponent, AppComponentContext by appComponentContext { private val model: SwapModel = getOrCreateModel(params) - private val bottomSheetSlot = childSlot( - source = model.bottomSheetNavigation, - serializer = AddToPortfolioRoute.serializer(), - key = BOTTOM_SHEET_SLOT_KEY, - handleBackButton = false, - childFactory = { _, context -> bottomSheetChild(context) }, - ) + // todo swap create InnerRouter + private val chooseTokenComponent by lazy { + chooseTokenComponentFactory.create( + context = child("chooseTokenComponent"), + params = ChooseTokenComponent.Params(model.chooseTokenBridge), + ) + } private val approvalSlot = childSlot( key = APPROVAL_SLOT_KEY, @@ -161,7 +158,6 @@ internal class DefaultSwapComponent @AssistedInject constructor( val feeSelectorChildStackState by childSlot.subscribeAsState() val feeSelectorBlockComponent = feeSelectorChildStackState.child?.instance - val bottomSheet by bottomSheetSlot.subscribeAsState() Crossfade( modifier = Modifier.background(TangemTheme.colors.background.secondary), @@ -189,38 +185,14 @@ internal class DefaultSwapComponent @AssistedInject constructor( ) } } - SwapNavScreen.SelectToken -> { - val tokenState = model.uiState.selectTokenState - if (tokenState != null) { - SwapSelectTokenScreen(state = tokenState, onBack = model.uiState.onBackClicked) - } else { - SwapScreen( - stateHolder = model.uiState, - feeSelectorBlockComponent = feeSelectorBlockComponent, - ) - } - } + SwapNavScreen.SelectToken -> chooseTokenComponent.Content(Modifier) } } - bottomSheet.child?.instance?.BottomSheet() - val approvalSlotState by approvalSlot.subscribeAsState() approvalSlotState.child?.instance?.BottomSheet() } - @Suppress("UnsafeCallOnNullableType") - private fun bottomSheetChild(componentContext: ComponentContext): ComposableBottomSheetComponent { - return addToPortfolioComponentFactory.create( - context = childByContext(componentContext), - params = AddToPortfolioComponent.Params( - addToPortfolioManager = model.addToPortfolioManager!!, - callback = model.addToPortfolioCallback, - shouldSkipTokenActionsScreen = true, - ), - ) - } - fun getApprovalParams(): GiveApprovalComponent.Params? { val permissionState = model.uiState.permissionState as? GiveTxPermissionState.ReadyForRequest ?: return null diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt new file mode 100644 index 0000000000..9caef19998 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt @@ -0,0 +1,82 @@ +package com.tangem.feature.swap.choosetoken.api + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +// todo swap make universal, encapsulate, move to some common module +internal interface ChooseTokenBridge { + + // todo swap new api + val onCurrencyChosen: Channel + val onClose: Channel + + // todo swap legacy api, remove + val onTokenSelected: Channel> + val onNewTokenAdded: Channel> + + val searchQueryState: StateFlow + val currenciesGroup: Flow + + fun onTokenSelected(tokenId: Pair) { + onTokenSelected.trySend(tokenId) + onSearchQuery("") + } + + fun onNewTokenAdded(addedToken: Pair) { + onNewTokenAdded.trySend(addedToken) + onSearchQuery("") + } + + fun onSearchQuery(query: String) + + fun updateCurrenciesGroup(currenciesGroup: CurrenciesGroup) + + fun onCurrencyChosen(result: ChooseTokenResult) { + onCurrencyChosen.trySend(result) + } + + fun onClose() { + onClose.trySend(Unit) + onSearchQuery("") + } + + interface Factory { + fun create(modelScope: CoroutineScope): ChooseTokenBridge + } +} + +data class ChooseTokenResult( + val addedCurrency: AccountCryptoCurrencyStatus, + val userWallet: UserWallet, + val analyticsPayload: Set = emptySet(), +) { + val status: CryptoCurrencyStatus get() = addedCurrency.status + val account: Account.CryptoPortfolio get() = addedCurrency.account + val walletId get() = userWallet.walletId +} + +sealed interface ChooseTokenAnalyticsPayload { + + @Suppress("BooleanPropertyNaming") + @JvmInline + value class IsSearched(val value: Boolean) : ChooseTokenAnalyticsPayload +} + +internal interface ChooseTokenComponent : ComposableContentComponent { + + data class Params( + val bridge: ChooseTokenBridge, + ) + + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/SettingContextUseCase.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/SettingContextUseCase.kt new file mode 100644 index 0000000000..a40a2a311c --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/SettingContextUseCase.kt @@ -0,0 +1,40 @@ +package com.tangem.feature.swap.choosetoken.api + +import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import javax.inject.Inject + +// todo swap move to some common module +class SettingContextUseCase @Inject constructor( + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, +) { + + suspend fun invokeSync(): SettingContext = invoke().first() + + operator fun invoke(): Flow = combine( + flow = isAccountsModeEnabledUseCase.invoke(), + flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), + flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(), + transform = { isAccountsModeEnabled, selectedAppCurrency, balanceHidingSettings -> + SettingContext( + isAccountsMode = isAccountsModeEnabled, + appCurrency = selectedAppCurrency, + isBalanceHidden = balanceHidingSettings, + ) + }, + ).distinctUntilChanged() +} + +data class SettingContext( + val isAccountsMode: Boolean, + val appCurrency: AppCurrency, + val isBalanceHidden: Boolean, +) \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt new file mode 100644 index 0000000000..870a5a7abe --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt @@ -0,0 +1,46 @@ +package com.tangem.feature.swap.choosetoken.impl + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload +import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge +import com.tangem.feature.swap.choosetoken.api.ChooseTokenResult +import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel.Companion.DEBOUNCE_SEARCH_DELAY +import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.* + +internal class DefaultChooseTokenBridge @AssistedInject constructor( + @Assisted private val modelScope: CoroutineScope, +) : ChooseTokenBridge { + + override val onCurrencyChosen: Channel = Channel() + + override val onTokenSelected: Channel> = Channel() + override val onNewTokenAdded: Channel> = Channel() + override val onClose: Channel = Channel() + + private val onSearchQuery: Channel = Channel() + override val searchQueryState: StateFlow = onSearchQuery.receiveAsFlow() + .debounce(DEBOUNCE_SEARCH_DELAY) + .stateIn(modelScope, SharingStarted.Eagerly, initialValue = "") + + private val _currenciesGroupFlow = MutableStateFlow(null) + override val currenciesGroup: Flow = _currenciesGroupFlow.filterNotNull() + + override fun onSearchQuery(query: String) { + onSearchQuery.trySend(query) + } + + override fun updateCurrenciesGroup(currenciesGroup: CurrenciesGroup) { + _currenciesGroupFlow.update { currenciesGroup } + } + + @AssistedFactory + interface Factory : ChooseTokenBridge.Factory { + override fun create(modelScope: CoroutineScope): DefaultChooseTokenBridge + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt new file mode 100644 index 0000000000..3ac09c489f --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt @@ -0,0 +1,72 @@ +package com.tangem.feature.swap.choosetoken.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +import com.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.feature.swap.choosetoken.api.ChooseTokenComponent +import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel +import com.tangem.feature.swap.models.AddToPortfolioRoute +import com.tangem.feature.swap.ui.SwapSelectTokenScreen +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultChooseTokenComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: ChooseTokenComponent.Params, + private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, +) : AppComponentContext by appComponentContext, ChooseTokenComponent { + + private val model: ChooseTokenModel = getOrCreateModel(params) + + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = AddToPortfolioRoute.serializer(), + key = BOTTOM_SHEET_SLOT_KEY, + handleBackButton = false, + childFactory = { _, context -> bottomSheetChild(context) }, + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() + state?.let { stateHolder -> + SwapSelectTokenScreen(state = stateHolder, onBack = { model.onBackClicked() }) + } + bottomSheet.child?.instance?.BottomSheet() + } + + @Suppress("UnsafeCallOnNullableType") + private fun bottomSheetChild(componentContext: ComponentContext): ComposableBottomSheetComponent { + return addToPortfolioComponentFactory.create( + context = childByContext(componentContext), + params = AddToPortfolioComponent.Params( + addToPortfolioManager = model.addToPortfolioManager!!, + callback = model.addToPortfolioCallback, + shouldSkipTokenActionsScreen = true, + ), + ) + } + + @AssistedFactory + interface Factory : ChooseTokenComponent.Factory { + override fun create( + context: AppComponentContext, + params: ChooseTokenComponent.Params, + ): DefaultChooseTokenComponent + } + + private companion object { + const val BOTTOM_SHEET_SLOT_KEY = "choosePortfolioTokenBottomSheetSlot" + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/di/ChooseTokenModule.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/di/ChooseTokenModule.kt new file mode 100644 index 0000000000..48e130b850 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/di/ChooseTokenModule.kt @@ -0,0 +1,39 @@ +package com.tangem.feature.swap.choosetoken.impl.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge +import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent +import com.tangem.feature.swap.choosetoken.impl.DefaultChooseTokenBridge +import com.tangem.feature.swap.choosetoken.impl.DefaultChooseTokenComponent +import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel +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(ModelComponent::class) +internal interface ChooseTokenModelModule { + + @Binds + @IntoMap + @ClassKey(ChooseTokenModel::class) + fun provideChooseTokenModel(model: ChooseTokenModel): Model +} + +@Module +@InstallIn(SingletonComponent::class) +internal interface ChooseTokenFeatureModule { + + @Binds + @Singleton + fun provideChooseTokenComponentFactory(impl: DefaultChooseTokenComponent.Factory): ChooseTokenComponent.Factory + + @Binds + @Singleton + fun provideDefaultChooseTokenBridgeFactory(impl: DefaultChooseTokenBridge.Factory): ChooseTokenBridge.Factory +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt new file mode 100644 index 0000000000..9348c16c67 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt @@ -0,0 +1,289 @@ +package com.tangem.feature.swap.choosetoken.impl.model + +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains +import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenMarketListConfig +import com.tangem.domain.markets.toSerializableParam +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload +import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge +import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent +import com.tangem.feature.swap.choosetoken.api.SettingContextUseCase +import com.tangem.feature.swap.converters.TokensDataConverter +import com.tangem.feature.swap.models.AddToPortfolioRoute +import com.tangem.feature.swap.models.SwapSelectTokenStateHolder +import com.tangem.feature.swap.models.market.MarketsListBatchFlowManager +import com.tangem.feature.swap.models.market.state.SwapMarketState +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Suppress("LongParameterList") +@ModelScoped +internal class ChooseTokenModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, + private val excludedBlockchains: ExcludedBlockchains, + private val getUserWalletsUseCase: GetWalletsUseCase, + private val settingContextUseCase: SettingContextUseCase, + private val marketsListBatchFlowManagerFactory: MarketsListBatchFlowManager.Factory, + paramsContainer: ParamsContainer, +) : Model() { + + private val params = paramsContainer.require() + private val bridge: ChooseTokenBridge = params.bridge + private val searchQueryState = bridge.searchQueryState + + private val addToPortfolioJobHolder = JobHolder() + + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + + private val visibleMarketItemIds = MutableStateFlow>(emptyList()) + private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) + + var addToPortfolioManager: AddToPortfolioManager? = null + val addToPortfolioCallback = object : AddToPortfolioComponent.Callback { + override fun onDismiss() = bottomSheetNavigation.dismiss() + + override fun onSuccess(addedToken: CryptoCurrency) { + modelScope.launch { + val newToken = addedToken to ChooseTokenAnalyticsPayload + .IsSearched(searchQueryState.value.isNotEmpty()) + bridge.onNewTokenAdded(newToken) + bottomSheetNavigation.dismiss() + } + } + } + + private val defaultMarketsListManager by lazy { + marketsListBatchFlowManagerFactory.create( + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, + order = TokenMarketListConfig.Order.Trending, + currentSearchText = Provider { null }, + modelScope = modelScope, + ) + } + + private val searchMarketsListManager by lazy { + marketsListBatchFlowManagerFactory.create( + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, + order = TokenMarketListConfig.Order.ByRating, + currentSearchText = Provider { searchQueryState.value }, + modelScope = modelScope, + ) + } + + val state: StateFlow = combineUI() + + init { + subscribeMarketTokens() + } + + private fun combineUI(): StateFlow = combine( + flow = bridge.currenciesGroup, + flow2 = settingContextUseCase.invoke(), + flow3 = marketsStateFlow(), + transform = { currenciesGroup, settingContext, marketState -> + val isAccountsMode = settingContext.isAccountsMode + val appCurrency = settingContext.appCurrency + val isBalanceHidden = settingContext.isBalanceHidden + TokensDataConverter( + onSearchEntered = { query -> bridge.onSearchQuery(query) }, + onTokenSelected = { tokenId -> + val selected = tokenId to ChooseTokenAnalyticsPayload + .IsSearched(searchQueryState.value.isNotEmpty()) + bridge.onTokenSelected(selected) + }, + tokensDataState = currenciesGroup, + isBalanceHidden = isBalanceHidden, + isAccountsMode = isAccountsMode, + appCurrency = appCurrency, + marketState = marketState, + ).transform() + }, + ) + .flowOn(dispatchers.default) + .stateIn(modelScope, SharingStarted.Eagerly, initialValue = null) + + private fun marketsStateFlow() = searchQueryState + // Switch between default and search market flows + .map { it.isEmpty() } + .distinctUntilChanged() + .flatMapLatest { isDefaultMode -> + if (isDefaultMode) { + visibleMarketItemIds.value = emptyList() + createDefaultMarketsFlow() + } else { + visibleDefaultMarketItemIds.value = emptyList() + createSearchMarketsFlow() + } + } + + private fun subscribeMarketTokens() { + // Reload search markets when query changes + searchQueryState + .onEach { searchQuery -> + if (searchQuery.isNotEmpty()) { + searchMarketsListManager.reload(searchQuery) + } + } + .launchIn(modelScope) + + // Initial load of default markets + defaultMarketsListManager.reload() + + visibleMarketItemIds + .mapNotNull { rawIDS -> + if (rawIDS.isNotEmpty()) { + searchMarketsListManager.getBatchKeysByItemIds(rawIDS) + } else { + null + } + } + .distinctUntilChanged() + .transformLatest, Unit> { visibleBatchKeys -> + searchMarketsListManager.loadCharts(visibleBatchKeys) + } + .launchIn(modelScope) + + visibleDefaultMarketItemIds + .mapNotNull { rawIds -> + if (rawIds.isNotEmpty()) { + defaultMarketsListManager.getBatchKeysByItemIds(rawIds) + } else { + null + } + }.distinctUntilChanged() + .transformLatest, Unit> { visibleBatchKeys -> + defaultMarketsListManager.loadCharts(visibleBatchKeys) + } + .launchIn(modelScope) + } + + private fun createDefaultMarketsFlow(): Flow { + val marketsTitle = TextReference.Res(R.string.feed_trending_now) + return combine( + defaultMarketsListManager.uiItems, + defaultMarketsListManager.isInInitialLoadingErrorState, + defaultMarketsListManager.totalCount, + ) { uiItems, isError, total -> + when { + isError -> SwapMarketState.LoadingError( + onRetryClicked = { defaultMarketsListManager.reload() }, + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + uiItems.isEmpty() -> SwapMarketState.DefaultLoading + else -> SwapMarketState.Content( + items = uiItems, + loadMore = { defaultMarketsListManager.loadMore() }, + onItemClick = { item -> addToPortfolioItem(item) }, + visibleIdsChanged = { visibleDefaultMarketItemIds.value = it }, + total = total ?: uiItems.size, + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + } + } + } + + private fun createSearchMarketsFlow(): Flow { + val marketsTitle = TextReference.Res(R.string.markets_common_title) + return combine( + flow = searchMarketsListManager.uiItems, + flow2 = searchMarketsListManager.isInInitialLoadingErrorState, + flow3 = searchMarketsListManager.isSearchNotFoundState, + flow4 = searchMarketsListManager.totalCount, + ) { uiItems, isError, isSearchNotFound, total -> + when { + isError -> SwapMarketState.LoadingError( + onRetryClicked = { searchMarketsListManager.reload(searchQueryState.value) }, + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) + isSearchNotFound -> SwapMarketState.SearchNothingFound + uiItems.isEmpty() -> SwapMarketState.SearchLoading + else -> SwapMarketState.Content( + items = uiItems, + loadMore = { searchMarketsListManager.loadMore() }, + onItemClick = { item -> addToPortfolioItem(item) }, + visibleIdsChanged = { visibleMarketItemIds.value = it }, + total = total ?: uiItems.size, + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) + } + } + } + + private fun addToPortfolioItem(item: MarketsListItemUM) { + modelScope.launch { + val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id) + ?: searchMarketsListManager.getTokenMarketById(item.id) + ?: return@launch + + val param = tokenMarket.toSerializableParam() + val hasOnlyHotWallets = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot } + + val networks = tokenMarket.networks?.filter { network -> + BlockchainUtils.isSupportedNetworkId( + blockchainId = network.networkId, + coinId = tokenMarket.id.value, + contractAddress = network.contractAddress, + excludedBlockchains = excludedBlockchains, + hotExcludedBlockchains = hotWalletExcludedBlockchains, + hasOnlyHotWallets = hasOnlyHotWallets, + ) + }?.map { network -> + TokenMarketInfo.Network( + networkId = network.networkId, + isExchangeable = false, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) + }.orEmpty() + + addToPortfolioManager = addToPortfolioManagerFactory + .create( + scope = modelScope, + token = param, + analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value), + ).apply { + setTokenNetworks(networks) + } + + addToPortfolioManager?.state + ?.firstOrNull { it is AddToPortfolioManager.State.AvailableToAdd } + ?.run { bottomSheetNavigation.activate(AddToPortfolioRoute) } + }.saveIn(addToPortfolioJobHolder) + } + + fun onBackClicked() { + bridge.onClose() + } + + companion object { + const val DEBOUNCE_SEARCH_DELAY = 500L + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index d54ae4ecc1..0b7cb9c397 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -5,76 +5,70 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup import com.tangem.feature.swap.models.SwapSelectTokenStateHolder -import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.TokenListUMData +import com.tangem.feature.swap.models.market.state.SwapMarketState import com.tangem.feature.swap.presentation.R -import com.tangem.utils.Provider -import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.plus import kotlinx.collections.immutable.toPersistentList +@Suppress("LongParameterList") internal class TokensDataConverter( private val onSearchEntered: (String) -> Unit, private val onTokenSelected: (String) -> Unit, private val tokensDataState: CurrenciesGroup, private val isBalanceHidden: Boolean, private val isAccountsMode: Boolean, - appCurrencyProvider: Provider, -) : Transformer { + private val appCurrency: AppCurrency, + private val marketState: SwapMarketState, +) { private val accountListItemConverter = AccountTokenItemConverter( - appCurrency = appCurrencyProvider(), + appCurrency = appCurrency, unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), onItemClick = onTokenSelected, ) - override fun transform(prevState: SwapStateHolder): SwapStateHolder { + fun transform(): SwapSelectTokenStateHolder { val accountList = tokensDataState.accountCurrencyList - val currentMarketsState = prevState.selectTokenState?.marketsState - return prevState.copy( - selectTokenState = SwapSelectTokenStateHolder( - availableTokens = persistentListOf(), - unavailableTokens = persistentListOf(), - tokensListData = if (isAccountsMode) { - val portfolioList = accountListItemConverter.convertList(accountList).toPersistentList() - val totalTokensCount = portfolioList.sumOf { it.tokens.size } - if (totalTokensCount > 0) { - TokenListUMData.AccountList( - tokensList = portfolioList, - totalTokensCount = totalTokensCount, - ) - } else { - TokenListUMData.EmptyList - } + return SwapSelectTokenStateHolder( + tokensListData = if (isAccountsMode) { + val portfolioList = accountListItemConverter.convertList(accountList).toPersistentList() + val totalTokensCount = portfolioList.sumOf { it.tokens.size } + if (totalTokensCount > 0) { + TokenListUMData.AccountList( + tokensList = portfolioList, + totalTokensCount = totalTokensCount, + ) } else { - val tokensList = accountList.flatMap { (_, currencyList) -> - currencyList.asSequence().map { accountSwapCurrency -> - accountListItemConverter.createAvailableItemConverter() - .convert(accountSwapCurrency.cryptoCurrencyStatus) - }.map(TokensListItemUM::Token).toPersistentList() - }.toPersistentList() + TokenListUMData.EmptyList + } + } else { + val tokensList = accountList.flatMap { (_, currencyList) -> + currencyList.asSequence().map { accountSwapCurrency -> + accountListItemConverter.createAvailableItemConverter() + .convert(accountSwapCurrency.cryptoCurrencyStatus) + }.map(TokensListItemUM::Token).toPersistentList() + }.toPersistentList() - if (tokensList.isNotEmpty()) { - TokenListUMData.TokenList( - tokensList = persistentListOf( - TokensListItemUM.GroupTitle( - id = "available_tokens_title", - text = resourceReference(R.string.exchange_tokens_available_tokens_header), - ), - ) + tokensList, - totalTokensCount = tokensList.size, - ) - } else { - TokenListUMData.EmptyList - } - }, - marketsState = currentMarketsState, - onSearchEntered = onSearchEntered, - onTokenSelected = onTokenSelected, - isBalanceHidden = isBalanceHidden, - isAfterSearch = tokensDataState.isAfterSearch, - ), + if (tokensList.isNotEmpty()) { + TokenListUMData.TokenList( + tokensList = persistentListOf( + TokensListItemUM.GroupTitle( + id = "available_tokens_title", + text = resourceReference(R.string.exchange_tokens_available_tokens_header), + ), + ) + tokensList, + totalTokensCount = tokensList.size, + ) + } else { + TokenListUMData.EmptyList + } + }, + marketsState = marketState, + onSearchEntered = onSearchEntered, + isBalanceHidden = isBalanceHidden, + isAfterSearch = tokensDataState.isAfterSearch, ) } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 05b8f74f9a..789816698c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -10,12 +10,10 @@ import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState.InProgress.getApproveTypeOrNull -import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -38,29 +36,25 @@ import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase +import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations.getCryptoCurrencyStatus import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.feedback.GetWalletMetaInfoUseCase 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.markets.GetMarketsTokenListFlowUseCase -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketListConfig -import com.tangem.domain.markets.toSerializableParam import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.WithdrawalResult +import com.tangem.domain.promo.ShouldShowStoriesUseCase import com.tangem.domain.promo.models.StoryContentIds import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry @@ -68,8 +62,6 @@ import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase -import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.promo.ShouldShowStoriesUseCase import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.transaction.error.GetFeeError @@ -77,8 +69,8 @@ import com.tangem.domain.transaction.models.TransactionFeeExtended import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.swap.analytics.SwapEvents +import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter import com.tangem.feature.swap.domain.SwapInteractor @@ -89,12 +81,9 @@ 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.ui.* -import com.tangem.feature.swap.models.AddToPortfolioRoute import com.tangem.feature.swap.models.SwapAlertUM import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.UiActions -import com.tangem.feature.swap.models.market.SwapMarketsListBatchFlowManager -import com.tangem.feature.swap.models.market.state.SwapMarketState import com.tangem.feature.swap.models.states.SwapNotificationUM import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter @@ -102,20 +91,16 @@ import com.tangem.feature.swap.ui.StateBuilder import com.tangem.feature.swap.utils.formatToUIRepresentation import com.tangem.features.approval.api.GiveApprovalComponent import com.tangem.features.approval.api.GiveApprovalFeatureToggles -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger import com.tangem.features.swap.SwapComponent -import com.tangem.features.swap.SwapFeatureToggles -import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.math.RoundingMode import java.text.DecimalFormat @@ -156,15 +141,11 @@ internal class SwapModel @Inject constructor( private val tangemPayWithdrawUseCase: TangemPayWithdrawUseCase, private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork, private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, - private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, - private val swapFeatureToggles: SwapFeatureToggles, - private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, - private val excludedBlockchains: ExcludedBlockchains, - private val getUserWalletsUseCase: GetWalletsUseCase, private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase, private val appsFlyerStore: AppsFlyerStore, private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles, private val messageSender: UiMessageSender, + chooseTokenBridgeFactory: ChooseTokenBridge.Factory, giveApprovalFeatureToggles: GiveApprovalFeatureToggles, ) : Model() { @@ -191,6 +172,8 @@ internal class SwapModel @Inject constructor( private val selectedAppCurrencyFlow: StateFlow = createSelectedAppCurrencyFlow() + val chooseTokenBridge: ChooseTokenBridge = chooseTokenBridgeFactory.create(modelScope) + private val stateBuilder = StateBuilder( userWalletProvider = Provider { userWallet }, actions = createUiActions(), @@ -207,7 +190,6 @@ internal class SwapModel @Inject constructor( ?: error("NumberFormat is not DecimalFormat"), ) private val amountDebouncer = Debouncer() - private val searchDebouncer = Debouncer() private val singleTaskScheduler = SingleTaskScheduler>() val dataStateStateFlow = MutableStateFlow(SwapProcessDataState()) @@ -256,44 +238,13 @@ internal class SwapModel @Inject constructor( private val fromTokenBalanceJobHolder = JobHolder() private val toTokenBalanceJobHolder = JobHolder() - private val addToPortfolioJobHolder = JobHolder() private var isAmountChangedByUser: Boolean = false private var lastPermissionNotificationTokens: Pair? = null - private val searchQueryState = MutableStateFlow("") - private val visibleMarketItemIds = MutableStateFlow>(emptyList()) - private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) - private var latestMarketsState: SwapMarketState? = null - - private val defaultMarketsListManager by lazy { - SwapMarketsListBatchFlowManager( - getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, - batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, - order = TokenMarketListConfig.Order.Trending, - currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, - currentSearchText = Provider { null }, - modelScope = modelScope, - dispatchers = dispatchers, - ) - } - - private val searchMarketsListManager by lazy { - SwapMarketsListBatchFlowManager( - getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, - batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, - order = TokenMarketListConfig.Order.ByRating, - currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, - currentSearchText = Provider { searchQueryState.value }, - modelScope = modelScope, - dispatchers = dispatchers, - ) - } - val currentScreen: SwapNavScreen get() = swapRouter.currentScreen - val bottomSheetNavigation: SlotNavigation = SlotNavigation() val approvalSlotNavigation = SlotNavigation() private val shouldUseGaslessApproval: Boolean = giveApprovalFeatureToggles.isGaslessApprovalEnabled @@ -321,35 +272,30 @@ internal class SwapModel @Inject constructor( } } - val addToPortfolioCallback = object : AddToPortfolioComponent.Callback { - override fun onDismiss() = bottomSheetNavigation.dismiss() - - override fun onSuccess(addedToken: CryptoCurrency) { - modelScope.launch { - bottomSheetNavigation.dismiss() - analyticsEventHandler.send( - SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = addedToken.symbol), - ) - analyticsEventHandler.send( - SwapAnalyticsEvent.TokenSelected( - token = addedToken.symbol, - source = ScreensSources.Markets, - isSearched = searchQueryState.value.isNotEmpty(), - ), - ) - searchQueryState.value = "" - getAccountCurrencyStatusUseCase.invoke(userWalletId, addedToken) - .firstOrNull { - it.status.value is CryptoCurrencyStatus.Loaded - }?.let { (account, status) -> - applyAddedToken(status, account) - } - } - } - } - var addToPortfolioManager: AddToPortfolioManager? = null - init { + chooseTokenBridge.searchQueryState + .onEach { query -> onSearchEntered(query) } + .launchIn(modelScope) + + chooseTokenBridge.onNewTokenAdded.receiveAsFlow() + .onEach { (addedToken, isSearched) -> + applyAddedToken(addedToken, isSearched.value) + } + .launchIn(modelScope) + + chooseTokenBridge.onTokenSelected.receiveAsFlow() + .onEach { (addedToken, isSearched) -> + onTokenSelect(addedToken, isSearched.value) + } + .launchIn(modelScope) + + chooseTokenBridge.onClose.receiveAsFlow() + .onEach { + analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(isTokenChosen = false)) + swapRouter.back() + } + .launchIn(modelScope) + modelScope.launch { val storyId = StoryContentIds.STORY_FIRST_TIME_SWAP.id if (shouldShowStoriesUseCase.invokeSync(storyId)) { @@ -423,32 +369,6 @@ internal class SwapModel @Inject constructor( uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden) } .launchIn(modelScope) - - subscribeMarketTokens() - - modelScope.launch { - visibleMarketItemIds.mapNotNull { rawIDS -> - if (rawIDS.isNotEmpty()) { - searchMarketsListManager.getBatchKeysByItemIds(rawIDS) - } else { - null - } - }.distinctUntilChanged().collectLatest { visibleBatchKeys -> - searchMarketsListManager.loadCharts(visibleBatchKeys) - } - } - - modelScope.launch { - visibleDefaultMarketItemIds.mapNotNull { rawIds -> - if (rawIds.isNotEmpty()) { - defaultMarketsListManager.getBatchKeysByItemIds(rawIds) - } else { - null - } - }.distinctUntilChanged().collectLatest { visibleBatchKeys -> - defaultMarketsListManager.loadCharts(visibleBatchKeys) - } - } } fun onStart() { @@ -474,41 +394,6 @@ internal class SwapModel @Inject constructor( analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(hasAvailableTokens = isAnyAvailableTokens)) } - private fun subscribeMarketTokens() { - if (swapFeatureToggles.isMarketListFeatureEnabled) { - // Switch between default and search market flows - searchQueryState - .map { it.isEmpty() } - .distinctUntilChanged() - .flatMapLatest { isDefaultMode -> - if (isDefaultMode) { - visibleMarketItemIds.value = emptyList() - createDefaultMarketsFlow() - } else { - visibleDefaultMarketItemIds.value = emptyList() - createSearchMarketsFlow() - } - } - .onEach { marketsState -> - latestMarketsState = marketsState - applyMarketsState(marketsState) - } - .launchIn(modelScope) - - // Reload search markets when query changes - searchQueryState - .onEach { searchQuery -> - if (searchQuery.isNotEmpty()) { - searchMarketsListManager.reload(searchQuery) - } - } - .launchIn(modelScope) - - // Initial load of default markets - defaultMarketsListManager.reload() - } - } - @Suppress("LongMethod") private fun initTokens(isReverseFromTo: Boolean) { modelScope.launch(dispatchers.main) { @@ -583,26 +468,40 @@ internal class SwapModel @Inject constructor( } } - private fun applyAddedToken(addedToken: CryptoCurrencyStatus, addedAccount: Account.CryptoPortfolio?) { - modelScope.launch { - runCatching(dispatchers.io) { - swapInteractor.getTokensDataState(initialCurrencyFrom) - }.onSuccess { state -> - updateTokensState(state) + private suspend fun applyAddedToken(addedToken: CryptoCurrency, isSearched: Boolean) { + analyticsEventHandler.send( + SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = addedToken.symbol), + ) + analyticsEventHandler.send( + SwapAnalyticsEvent.TokenSelected( + token = addedToken.symbol, + source = ScreensSources.Markets, + isSearched = isSearched, + ), + ) + val status = getAccountCurrencyStatusUseCase.invoke(userWalletId, addedToken) + // todo swap are sure?? about status.value is CryptoCurrencyStatus.Loaded + .firstOrNull { it.status.value is CryptoCurrencyStatus.Loaded } + ?: return + val (selectedAccount, selectedCurrency) = status - applyInitialTokenChoice( - state = state, - selectedCurrency = addedToken, - selectedAccount = addedAccount, - isReverseFromTo = isOrderReversed, - ) + runCatching(dispatchers.io) { + swapInteractor.getTokensDataState(initialCurrencyFrom) + }.onSuccess { state -> + updateTokensState(state) - subscribeToCoinBalanceUpdatesIfNeeded() + applyInitialTokenChoice( + state = state, + selectedCurrency = selectedCurrency, + selectedAccount = selectedAccount, + isReverseFromTo = isOrderReversed, + ) - swapRouter.back() - }.onFailure { error -> - TangemLogger.e("Error", error) - } + subscribeToCoinBalanceUpdatesIfNeeded() + + swapRouter.back() + }.onFailure { error -> + TangemLogger.e("Error", error) } } @@ -686,23 +585,7 @@ internal class SwapModel @Inject constructor( private fun updateTokensState(tokenDataState: TokensDataStateExpress) { val tokensDataState = if (isOrderReversed) tokenDataState.fromGroup else tokenDataState.toGroup - - uiState = stateBuilder.addTokensToStateV2( - uiState = uiState, - tokensDataState = tokensDataState, - isAccountsMode = isAccountsMode, - ) - latestMarketsState?.let(::applyMarketsState) - } - - private fun applyMarketsState(marketsState: SwapMarketState) { - uiState.selectTokenState?.let { currentSelectState -> - uiState = uiState.copy( - selectTokenState = currentSelectState.copy( - marketsState = marketsState, - ), - ) - } + chooseTokenBridge.updateCurrenciesGroup(tokensDataState) } private fun startLoadingQuotes( @@ -1274,65 +1157,61 @@ internal class SwapModel @Inject constructor( } private fun onSearchEntered(searchQuery: String) { - searchDebouncer.debounce(modelScope, DEBOUNCE_SEARCH_DELAY) { - searchQueryState.value = searchQuery - - val tokenDataState = dataState.tokensDataState ?: return@debounce - val group = if (isOrderReversed) { - tokenDataState.fromGroup - } else { - tokenDataState.toGroup - } - - val available = group.available.filter { swapAvailability -> - swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || - swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) - } - val unavailable = group.unavailable.filter { swapAvailability -> - swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || - swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) - } - val accountCurrencyList = group.accountCurrencyList.mapNotNull { accountSwapAvailability -> - val filteredCurrencies = accountSwapAvailability.currencyList.filter { accountSwapCurrency -> - val currency = accountSwapCurrency.cryptoCurrencyStatus.currency - currency.name.contains(searchQuery, ignoreCase = true) || - currency.symbol.contains(searchQuery, ignoreCase = true) - } - - if (filteredCurrencies.isEmpty()) { - return@mapNotNull null - } - - accountSwapAvailability.copy( - currencyList = filteredCurrencies, - ) - } - - val filteredTokenDataState = if (isOrderReversed) { - tokenDataState.copy( - fromGroup = tokenDataState.fromGroup.copy( - available = available, - unavailable = unavailable, - accountCurrencyList = accountCurrencyList, - isAfterSearch = true, - ), - ) - } else { - tokenDataState.copy( - toGroup = tokenDataState.toGroup.copy( - available = available, - unavailable = unavailable, - accountCurrencyList = accountCurrencyList, - isAfterSearch = true, - ), - ) - } - updateTokensState(filteredTokenDataState) + val tokenDataState = dataState.tokensDataState ?: return + val group = if (isOrderReversed) { + tokenDataState.fromGroup + } else { + tokenDataState.toGroup } + + val available = group.available.filter { swapAvailability -> + swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || + swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) + } + val unavailable = group.unavailable.filter { swapAvailability -> + swapAvailability.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || + swapAvailability.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) + } + val accountCurrencyList = group.accountCurrencyList.mapNotNull { accountSwapAvailability -> + val filteredCurrencies = accountSwapAvailability.currencyList.filter { accountSwapCurrency -> + val currency = accountSwapCurrency.cryptoCurrencyStatus.currency + currency.name.contains(searchQuery, ignoreCase = true) || + currency.symbol.contains(searchQuery, ignoreCase = true) + } + + if (filteredCurrencies.isEmpty()) { + return@mapNotNull null + } + + accountSwapAvailability.copy( + currencyList = filteredCurrencies, + ) + } + + val filteredTokenDataState = if (isOrderReversed) { + tokenDataState.copy( + fromGroup = tokenDataState.fromGroup.copy( + available = available, + unavailable = unavailable, + accountCurrencyList = accountCurrencyList, + isAfterSearch = true, + ), + ) + } else { + tokenDataState.copy( + toGroup = tokenDataState.toGroup.copy( + available = available, + unavailable = unavailable, + accountCurrencyList = accountCurrencyList, + isAfterSearch = true, + ), + ) + } + updateTokensState(filteredTokenDataState) } @Suppress("LongMethod") - private fun onTokenSelect(id: String) { + private fun onTokenSelect(id: String, isSearched: Boolean) { val tokens = dataState.tokensDataState ?: return val (foundToken, foundAccount) = getSelectedTokenAndAccount(tokens, id) @@ -1345,7 +1224,7 @@ internal class SwapModel @Inject constructor( SwapAnalyticsEvent.TokenSelected( token = symbol, source = ScreensSources.Portfolio, - isSearched = searchQueryState.value.isNotEmpty(), + isSearched = isSearched, ), ) } @@ -1709,8 +1588,6 @@ internal class SwapModel @Inject constructor( @Suppress("LongMethod", "CyclomaticComplexMethod") private fun createUiActions(): UiActions { return UiActions( - onSearchEntered = { onSearchEntered(it) }, - onTokenSelected = { onTokenSelect(it) }, onAmountChanged = { onAmountChanged(it) }, onSwapClick = { onSwapClick() @@ -1738,9 +1615,6 @@ internal class SwapModel @Inject constructor( if (bottomSheet != null && bottomSheet.isShown) { uiState = stateBuilder.dismissBottomSheet(uiState) } else { - if (swapRouter.currentScreen == SwapNavScreen.SelectToken) { - analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(isTokenChosen = false)) - } swapRouter.back() } onSearchEntered("") @@ -2180,110 +2054,6 @@ internal class SwapModel @Inject constructor( } } - private fun createDefaultMarketsFlow(): Flow { - val marketsTitle = TextReference.Res(R.string.feed_trending_now) - return combine( - defaultMarketsListManager.uiItems, - defaultMarketsListManager.isInInitialLoadingErrorState, - defaultMarketsListManager.totalCount, - ) { uiItems, isError, total -> - when { - isError -> SwapMarketState.LoadingError( - onRetryClicked = { defaultMarketsListManager.reload() }, - marketsTitle = marketsTitle, - shouldAssetsCount = false, - ) - uiItems.isEmpty() -> SwapMarketState.Loading( - marketsTitle = marketsTitle, - shouldAssetsCount = false, - ) - else -> SwapMarketState.Content( - items = uiItems, - loadMore = { defaultMarketsListManager.loadMore() }, - onItemClick = { item -> addToPortfolioItem(item) }, - visibleIdsChanged = { visibleDefaultMarketItemIds.value = it }, - total = total ?: uiItems.size, - marketsTitle = marketsTitle, - shouldAssetsCount = false, - ) - } - } - } - - private fun createSearchMarketsFlow(): Flow { - val marketsTitle = TextReference.Res(R.string.markets_common_title) - return combine( - flow = searchMarketsListManager.uiItems, - flow2 = searchMarketsListManager.isInInitialLoadingErrorState, - flow3 = searchMarketsListManager.isSearchNotFoundState, - flow4 = searchMarketsListManager.totalCount, - ) { uiItems, isError, isSearchNotFound, total -> - when { - isError -> SwapMarketState.LoadingError( - onRetryClicked = { searchMarketsListManager.reload(searchQueryState.value) }, - marketsTitle = marketsTitle, - shouldAssetsCount = true, - ) - isSearchNotFound -> SwapMarketState.SearchNothingFound - uiItems.isEmpty() -> SwapMarketState.Loading( - marketsTitle = marketsTitle, - shouldAssetsCount = true, - ) - else -> SwapMarketState.Content( - items = uiItems, - loadMore = { searchMarketsListManager.loadMore() }, - onItemClick = { item -> addToPortfolioItem(item) }, - visibleIdsChanged = { visibleMarketItemIds.value = it }, - total = total ?: uiItems.size, - marketsTitle = marketsTitle, - shouldAssetsCount = true, - ) - } - } - } - - private fun addToPortfolioItem(item: MarketsListItemUM) { - modelScope.launch { - val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id) - ?: searchMarketsListManager.getTokenMarketById(item.id) - ?: return@launch - - val param = tokenMarket.toSerializableParam() - val hasOnlyHotWallets = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot } - - val networks = tokenMarket.networks?.filter { network -> - BlockchainUtils.isSupportedNetworkId( - blockchainId = network.networkId, - coinId = tokenMarket.id.value, - contractAddress = network.contractAddress, - excludedBlockchains = excludedBlockchains, - hotExcludedBlockchains = hotWalletExcludedBlockchains, - hasOnlyHotWallets = hasOnlyHotWallets, - ) - }?.map { network -> - TokenMarketInfo.Network( - networkId = network.networkId, - isExchangeable = false, - contractAddress = network.contractAddress, - decimalCount = network.decimalCount, - ) - }.orEmpty() - - addToPortfolioManager = addToPortfolioManagerFactory - .create( - scope = modelScope, - token = param, - analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value), - ).apply { - setTokenNetworks(networks) - } - - addToPortfolioManager?.state - ?.firstOrNull { it is AddToPortfolioManager.State.AvailableToAdd } - ?.run { bottomSheetNavigation.activate(AddToPortfolioRoute) } - }.saveIn(addToPortfolioJobHolder) - } - private fun CryptoCurrency.getNetworkInfo(): NetworkInfo { return NetworkInfo( name = this.network.name, @@ -2480,7 +2250,6 @@ internal class SwapModel @Inject constructor( const val INITIAL_AMOUNT = "" const val UPDATE_DELAY = 10000L const val DEBOUNCE_AMOUNT_DELAY = 1000L - const val DEBOUNCE_SEARCH_DELAY = 500L const val UPDATE_BALANCE_DELAY_MILLIS = 11000L const val SWAP_IN_PROGRESS_DELAY = 200L const val CHANGELLY_PROVIDER_ID = "changelly" diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt index 96d1358d0a..950ef764ad 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt @@ -1,43 +1,20 @@ package com.tangem.feature.swap.models -import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM -import com.tangem.core.ui.extensions.TextReference import com.tangem.feature.swap.models.market.state.SwapMarketState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf internal data class SwapSelectTokenStateHolder( - val availableTokens: ImmutableList, - val unavailableTokens: ImmutableList, - val marketsState: SwapMarketState? = null, + val marketsState: SwapMarketState, val tokensListData: TokenListUMData, val isBalanceHidden: Boolean, val isAfterSearch: Boolean, val onSearchEntered: (String) -> Unit, - val onTokenSelected: (String) -> Unit, -) - -internal sealed class TokenToSelectState { - - data class Title(val title: TextReference) : TokenToSelectState() - - data class TokenToSelect( - val id: String, - val name: String, - val symbol: String, - val tokenIcon: CurrencyIconState, - val isAvailable: Boolean = true, - val addedTokenBalanceData: TokenBalanceData? = null, - ) : TokenToSelectState() -} - -internal data class TokenBalanceData( - val amount: String?, - val amountEquivalent: String?, - val isBalanceHidden: Boolean, ) +@Immutable internal sealed interface TokenListUMData { val tokensList: ImmutableList @@ -64,11 +41,11 @@ internal sealed interface TokenListUMData { } internal val SwapSelectTokenStateHolder.isNotFoundState: Boolean - get() = availableTokens.isEmpty() && unavailableTokens.isEmpty() && + get() = tokensListData.tokensList.isEmpty() && isAfterSearch && - marketsState !is SwapMarketState.Content && marketsState !is SwapMarketState.Loading + marketsState !is SwapMarketState.Content && marketsState !is SwapMarketState.Loading internal val SwapSelectTokenStateHolder.isEmptyState: Boolean - get() = availableTokens.isEmpty() && unavailableTokens.isEmpty() && + get() = tokensListData.tokensList.isEmpty() && !isAfterSearch && - marketsState !is SwapMarketState.Content && marketsState !is SwapMarketState.Loading \ No newline at end of file + marketsState !is SwapMarketState.Content && marketsState !is SwapMarketState.Loading \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index dbf80b6efc..f3d9a7bc7d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -28,7 +28,6 @@ internal data class SwapStateHolder( val priceImpact: PriceImpact, val successState: SwapSuccessStateHolder? = null, - val selectTokenState: SwapSelectTokenStateHolder? = null, val bottomSheetConfig: TangemBottomSheetConfig? = null, val swapButton: SwapButton, val shouldShowMaxAmount: Boolean, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt index 895a4de52c..f6bddc0a70 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -7,8 +7,6 @@ import com.tangem.feature.swap.domain.models.ui.TxFee import java.math.BigDecimal data class UiActions( - val onSearchEntered: (String) -> Unit, - val onTokenSelected: (String) -> Unit, val onAmountChanged: (String) -> Unit, val onAmountSelected: (Boolean) -> Unit, val onSwapClick: () -> Unit, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/MarketsListBatchFlowManager.kt similarity index 88% rename from features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt rename to features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/MarketsListBatchFlowManager.kt index 77609fade7..64d320b24a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/MarketsListBatchFlowManager.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.models.market import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.* import com.tangem.domain.models.currency.CryptoCurrency @@ -12,6 +13,9 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider 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.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -19,14 +23,14 @@ import kotlinx.coroutines.* import kotlinx.coroutines.flow.* @Suppress("LongParameterList") -internal class SwapMarketsListBatchFlowManager( +internal class MarketsListBatchFlowManager @AssistedInject constructor( getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, - private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType, - private val order: TokenMarketListConfig.Order, - private val currentAppCurrency: Provider, - private val currentSearchText: Provider, - private val modelScope: CoroutineScope, + getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val dispatchers: CoroutineDispatcherProvider, + @Assisted private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType, + @Assisted private val order: TokenMarketListConfig.Order, + @Assisted private val currentSearchText: Provider, + @Assisted private val modelScope: CoroutineScope, ) { private val actionsFlow = MutableSharedFlow>(replay = 1) @@ -43,6 +47,9 @@ internal class SwapMarketsListBatchFlowManager( private val resultBatches = MutableStateFlow(ResultBatches()) private val uiBatches = resultBatches.map { it.uiBatches } + private val appCurrency: StateFlow = getSelectedAppCurrencyUseCase.invokeOrDefault() + .stateIn(modelScope, SharingStarted.Eagerly, AppCurrency.Default) + val uiItems: StateFlow> get() = uiBatches .map { batches -> @@ -114,7 +121,7 @@ internal class SwapMarketsListBatchFlowManager( val items = resultBatches.uiBatches val previousList = resultBatches.processedItems - val converter = SwapMarketsTokenItemConverter(appCurrency = currentAppCurrency()) + val converter = SwapMarketsTokenItemConverter(appCurrency = appCurrency.value) if (newList.isEmpty()) { return@update ResultBatches(processedItems = emptyList()) @@ -178,7 +185,7 @@ internal class SwapMarketsListBatchFlowManager( actionsFlow.emit( BatchAction.Reload( requestParams = TokenMarketListConfig( - fiatPriceCurrency = currentAppCurrency().code, + fiatPriceCurrency = appCurrency.value.code, searchText = if (currentSearchText() == null) { null } else { @@ -220,7 +227,7 @@ internal class SwapMarketsListBatchFlowManager( keys = batchesKeysToLoad, updateRequest = TokenMarketUpdateRequest.UpdateChart( interval = TokenMarketListConfig.Interval.H24, - currency = currentAppCurrency().code, + currency = appCurrency.value.code, ), async = true, operationId = batchesKeysToLoad.toString() + "h24", @@ -250,4 +257,14 @@ internal class SwapMarketsListBatchFlowManager( val uiBatches: List>> = emptyList(), val processedItems: List>>? = null, ) + + @AssistedFactory + interface Factory { + fun create( + batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType, + order: TokenMarketListConfig.Order, + currentSearchText: Provider, + modelScope: CoroutineScope, + ): MarketsListBatchFlowManager + } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt index 8be842bcfd..e18aad5dd7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt @@ -2,9 +2,9 @@ package com.tangem.feature.swap.models.market.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.core.ui.R import kotlinx.collections.immutable.ImmutableList @Immutable @@ -38,4 +38,17 @@ internal sealed class SwapMarketState { override val marketsTitle: TextReference = TextReference.Res(R.string.markets_common_title) override val shouldAssetsCount: Boolean = true } + + companion object { + val DefaultLoading + get() = Loading( + marketsTitle = TextReference.Res(R.string.feed_trending_now), + shouldAssetsCount = false, + ) + val SearchLoading + get() = Loading( + marketsTitle = TextReference.Res(R.string.markets_common_title), + shouldAssetsCount = true, + ) + } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 6d5e80eb4a..e65fefdf6c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -23,7 +23,6 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork -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.ExchangeProviderType @@ -658,21 +657,6 @@ internal class StateBuilder( ) } - fun addTokensToStateV2( - uiState: SwapStateHolder, - tokensDataState: CurrenciesGroup, - isAccountsMode: Boolean, - ): SwapStateHolder { - return TokensDataConverter( - onSearchEntered = actions.onSearchEntered, - onTokenSelected = actions.onTokenSelected, - appCurrencyProvider = appCurrencyProvider, - tokensDataState = tokensDataState, - isBalanceHidden = isBalanceHiddenProvider(), - isAccountsMode = isAccountsMode, - ).transform(uiState) - } - fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder { return uiState.copy( changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, @@ -757,28 +741,10 @@ internal class StateBuilder( val patchedReceiveCardData = uiState.receiveCardData.copy( isBalanceHidden = isBalanceHidden, ) - val selectTokenState = uiState.selectTokenState?.copy( - isBalanceHidden = isBalanceHidden, - availableTokens = uiState.selectTokenState.availableTokens.map { tokenState -> - when (tokenState) { - is TokenToSelectState.TokenToSelect -> { - tokenState.copy( - addedTokenBalanceData = tokenState.addedTokenBalanceData?.copy( - isBalanceHidden = isBalanceHidden, - ), - ) - } - is TokenToSelectState.Title -> { - tokenState - } - } - }.toImmutableList(), - ) return uiState.copy( sendCardData = patchedSendCardData, receiveCardData = patchedReceiveCardData, - selectTokenState = selectTokenState, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 2a21dd13ec..a117b2df9c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -3,7 +3,6 @@ package com.tangem.feature.swap.ui import androidx.activity.compose.BackHandler import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope @@ -26,19 +25,13 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle 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.SpacerH12 -import com.tangem.core.ui.components.SpacerH32 -import com.tangem.core.ui.components.SpacerW2 import com.tangem.core.ui.components.appbar.ExpandableSearchView import com.tangem.core.ui.components.list.InfiniteListHandler -import com.tangem.core.ui.components.atoms.text.EllipsisText -import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.tokenlist.PortfolioListItem import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem import com.tangem.core.ui.components.tokenlist.TokenListItem @@ -51,7 +44,6 @@ import com.tangem.core.ui.test.BuyTokenScreenTestTags import com.tangem.core.ui.utils.lazyListItemPosition import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenListUMData -import com.tangem.feature.swap.models.TokenToSelectState import com.tangem.feature.swap.models.isEmptyState import com.tangem.feature.swap.models.isNotFoundState import com.tangem.feature.swap.models.market.state.SwapMarketState @@ -59,7 +51,6 @@ import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.ui.market.swapMarketsListItems import com.tangem.feature.swap.ui.preview.SwapSelectTokenPreviewProvider import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf private const val LOAD_MORE_BUFFER = 25 @@ -162,12 +153,6 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = tokensListData = state.tokensListData, isBalanceHidden = state.isBalanceHidden, ) - - tokensToSelectItems(state.availableTokens, state.onTokenSelected) - - item { SpacerH12() } - - tokensToSelectItems(state.unavailableTokens, state.onTokenSelected) } } @@ -197,20 +182,6 @@ private fun ListOfTokensWithMarkets( isBalanceHidden = state.isBalanceHidden, ) - tokensToSelectItems(state.availableTokens, state.onTokenSelected) - if (state.unavailableTokens.isNotEmpty()) { - item { SpacerH12() } - tokensToSelectItems(state.unavailableTokens, state.onTokenSelected) - } - - val hasPortfolioContent = state.tokensListData !is TokenListUMData.EmptyList || - state.availableTokens.isNotEmpty() - if (hasPortfolioContent) { - item { SpacerH32() } - } else { - item { SpacerH12() } - } - swapMarketsListItems(marketsState) } @@ -374,170 +345,25 @@ private fun LazyListScope.portfolioItem( } } -private fun LazyListScope.tokensToSelectItems( - items: ImmutableList, - onTokenClick: (String) -> Unit, -) { - itemsIndexed(items = items) { index, item -> - when (item) { - is TokenToSelectState.Title -> { - TitleHeader( - item = item, - modifier = Modifier.roundedShapeItemDecoration( - currentIndex = index, - lastIndex = items.lastIndex, - ), - ) - } - is TokenToSelectState.TokenToSelect -> { - TokenItem( - token = item, - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = items.lastIndex, - ) - .background(TangemTheme.colors.background.action), - onTokenClick = { - onTokenClick(item.id) - }, - ) - } - } - } -} - -@Composable -private fun TitleHeader(item: TokenToSelectState.Title, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .fillMaxWidth() - .background(TangemTheme.colors.background.action), - ) { - Text( - text = item.title.resolveReference().uppercase(), - style = TangemTheme.typography.overline, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier - .padding( - top = TangemTheme.dimens.spacing16, - start = TangemTheme.dimens.spacing16, - ), - ) - } -} - -@Suppress("LongMethod") -@Composable -private fun TokenItem( - token: TokenToSelectState.TokenToSelect, - onTokenClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .fillMaxWidth() - .height(TangemTheme.dimens.size72) - .clickable( - enabled = token.isAvailable, - onClick = onTokenClick, - ) - .padding( - vertical = TangemTheme.dimens.spacing14, - horizontal = TangemTheme.dimens.spacing16, - ), - verticalAlignment = Alignment.CenterVertically, - ) { - CurrencyIcon( - state = token.tokenIcon, - shouldDisplayNetwork = true, - ) - - Column( - modifier = Modifier - .weight(1f) - .align(Alignment.CenterVertically) - .padding(start = TangemTheme.dimens.spacing12), - ) { - EllipsisText( - text = token.name, - style = TangemTheme.typography.subtitle1, - color = if (token.isAvailable) { - TangemTheme.colors.text.primary1 - } else { - TangemTheme.colors.text.tertiary - }, - ) - SpacerW2() - EllipsisText( - text = token.symbol, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - - if (token.addedTokenBalanceData != null) { - Column( - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.Center, - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing8), - ) { - Text( - text = token.addedTokenBalanceData.amountEquivalent.orEmpty().orMaskWithStars( - maskWithStars = token.addedTokenBalanceData.isBalanceHidden && - !token.addedTokenBalanceData.amountEquivalent.isNullOrEmpty(), - ), - style = TangemTheme.typography.subtitle1, - maxLines = 1, - softWrap = false, - overflow = TextOverflow.Visible, - color = if (token.isAvailable) { - TangemTheme.colors.text.primary1 - } else { - TangemTheme.colors.text.tertiary - }, - ) - SpacerW2() - Text( - text = token.addedTokenBalanceData.amount.orEmpty().orMaskWithStars( - maskWithStars = token.addedTokenBalanceData.isBalanceHidden && - !token.addedTokenBalanceData.amount.isNullOrEmpty(), - ), - maxLines = 1, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - } - } -} - private class SwapSelectTokenScreenPreviewProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf( // Content state with tokens and markets SwapSelectTokenPreviewProvider().provideSwapSelectTokenState(), // Empty state SwapSelectTokenStateHolder( - availableTokens = persistentListOf(), - unavailableTokens = persistentListOf(), tokensListData = TokenListUMData.EmptyList, - marketsState = null, + marketsState = SwapMarketState.DefaultLoading, isAfterSearch = false, isBalanceHidden = false, onSearchEntered = {}, - onTokenSelected = {}, ), // Not found state SwapSelectTokenStateHolder( - availableTokens = persistentListOf(), - unavailableTokens = persistentListOf(), tokensListData = TokenListUMData.EmptyList, - marketsState = null, + marketsState = SwapMarketState.SearchLoading, isAfterSearch = true, isBalanceHidden = false, onSearchEntered = {}, - onTokenSelected = {}, ), ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt index 8c03c69176..632255291e 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt @@ -2,16 +2,13 @@ package com.tangem.feature.swap.ui.preview import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.common.ui.markets.models.MarketsListItemUM -import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.core.ui.R import com.tangem.feature.swap.models.SwapSelectTokenStateHolder -import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenListUMData -import com.tangem.feature.swap.models.TokenToSelectState import com.tangem.feature.swap.models.market.state.SwapMarketState import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.persistentListOf @@ -20,13 +17,10 @@ internal class SwapSelectTokenPreviewProvider { fun provideSwapSelectTokenState(): SwapSelectTokenStateHolder { return SwapSelectTokenStateHolder( - availableTokens = listOf(previewTitle, previewToken, previewToken, previewToken).toImmutableList(), - unavailableTokens = listOf(previewTitle, previewToken, previewToken, previewToken).toImmutableList(), tokensListData = TokenListUMData.EmptyList, isAfterSearch = false, isBalanceHidden = false, onSearchEntered = {}, - onTokenSelected = {}, marketsState = createPreviewMarketsState(), ) } @@ -136,26 +130,5 @@ internal class SwapSelectTokenPreviewProvider { CHART_VALUE_6, ), ) - - private val previewToken = TokenToSelectState.TokenToSelect( - tokenIcon = CurrencyIconState.CoinIcon( - url = "", - fallbackResId = 0, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - id = "", - name = "Optimistic Ethereum (ETH)", - symbol = "USDC", - addedTokenBalanceData = TokenBalanceData( - amount = "15 000 $", - amountEquivalent = "15 000 USDT", - isBalanceHidden = false, - ), - ) - - private val previewTitle = TokenToSelectState.Title( - title = stringReference("MY TOKENS"), - ) } } \ No newline at end of file From c6b2e89890fe416e746c50e97f6c6c87269d720f Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Mar 2026 08:24:24 +0500 Subject: [PATCH 27/75] Updated on 2026-08-14 --- .../data/quote/MockQuoteResponseFactory.kt | 1 + .../test/data/quote/QuoteResponseExt.kt | 1 + .../api/tangemTech/models/QuotesResponse.kt | 3 + .../tangem/data/common/quote/QuotesFetcher.kt | 3 +- .../converters/HotCryptoCurrencyConverter.kt | 1 + .../quotes/converter/QuoteStatusConverter.kt | 1 + .../tangem/data/quotes/di/QuotesDataModule.kt | 9 ++- .../multi/DefaultMultiQuoteStatusFetcher.kt | 2 +- .../repository/DefaultQuotesRepository.kt | 4 ++ .../converter/QuoteStatusConverterTest.kt | 6 ++ .../DefaultMultiQuoteStatusFetcherTest.kt | 2 +- .../repository/DefaultQuotesRepositoryTest.kt | 67 +++++++++++++++++++ .../DefaultSingleQuoteStatusProducerTest.kt | 2 + .../tangem/domain/models/quote/QuoteStatus.kt | 2 + .../quotes/GetCurrencyUSDQuoteUseCase.kt | 24 +++++++ .../tangem/domain/quotes/QuotesRepository.kt | 3 + .../tangem/domain/tokens/mock/MockQuotes.kt | 10 +++ .../CryptoCurrencyStatusFactoryTest.kt | 1 + .../supply/YieldSupplyMinAmountUseCaseTest.kt | 1 + .../YieldSupplyGetCurrentFeeUseCaseTest.kt | 3 + 20 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 domain/quotes/src/main/java/com/tangem/domain/quotes/GetCurrencyUSDQuoteUseCase.kt diff --git a/common/test/src/main/java/com/tangem/common/test/data/quote/MockQuoteResponseFactory.kt b/common/test/src/main/java/com/tangem/common/test/data/quote/MockQuoteResponseFactory.kt index 2e3f56492e..62985a6bf9 100644 --- a/common/test/src/main/java/com/tangem/common/test/data/quote/MockQuoteResponseFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/data/quote/MockQuoteResponseFactory.kt @@ -14,6 +14,7 @@ object MockQuoteResponseFactory { priceChange24h = value, priceChange1w = value, priceChange30d = value, + priceUsd = value, ) } } \ No newline at end of file diff --git a/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt b/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt index 67ff49b2b2..1e50e6ae60 100644 --- a/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt +++ b/common/test/src/main/java/com/tangem/common/test/data/quote/QuoteResponseExt.kt @@ -13,6 +13,7 @@ fun QuotesResponse.Quote.toDomain(rawCurrencyId: String, source: StatusSource = source = source, fiatRate = price.orZero(), priceChange = priceChange24h.orZero().movePointLeft(2), + fiatRateUSD = priceUsd.orZero(), ), ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt index b9b4460255..26ef674849 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/QuotesResponse.kt @@ -20,6 +20,8 @@ data class QuotesResponse( val priceChange1w: BigDecimal?, @Json(name = "priceChange30d") val priceChange30d: BigDecimal?, + @Json(name = "priceUsd") + val priceUsd: BigDecimal?, ) { companion object { @@ -29,6 +31,7 @@ data class QuotesResponse( priceChange24h = null, priceChange1w = null, priceChange30d = null, + priceUsd = null, ) } } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/quote/QuotesFetcher.kt b/data/common/src/main/kotlin/com/tangem/data/common/quote/QuotesFetcher.kt index bc89150de8..55c4a5807c 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/quote/QuotesFetcher.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/quote/QuotesFetcher.kt @@ -43,8 +43,9 @@ interface QuotesFetcher { PRICE_CHANGE_24H(value = "priceChange24h"), PRICE_CHANGE_1W(value = "priceChange1w"), PRICE_CHANGE_30D(value = "priceChange30d"), + PRICE_USD(value = "priceUsd"), ALL_PRICES( - value = setOf(PRICE, PRICE_CHANGE_24H, PRICE_CHANGE_1W, PRICE_CHANGE_30D).combine(), + value = setOf(PRICE, PRICE_CHANGE_24H, PRICE_CHANGE_1W, PRICE_CHANGE_30D, PRICE_USD).combine(), ), LAST_UPDATED_AT(value = "lastUpdatedAt"), } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt index 256cc0c439..8311e2cdce 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt @@ -95,6 +95,7 @@ internal class HotCryptoCurrencyConverter( rawCurrencyId = rawCurrencyId, value = QuoteStatus.Data( fiatRate = fiatRate, + fiatRateUSD = BigDecimal.ZERO, priceChange = priceChange.movePointLeft(2), source = StatusSource.ACTUAL, // It doesn't matter ), diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/converter/QuoteStatusConverter.kt b/data/quotes/src/main/java/com/tangem/data/quotes/converter/QuoteStatusConverter.kt index 6fe470b39a..163618af88 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/converter/QuoteStatusConverter.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/converter/QuoteStatusConverter.kt @@ -27,6 +27,7 @@ internal class QuoteStatusConverter( source = source, fiatRate = quote.price.orZero(), priceChange = quote.priceChange24h.orZero().movePointLeft(2), + fiatRateUSD = quote.priceUsd.orZero(), ), ) } diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt index f7e09bf552..ffd7ce636e 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/di/QuotesDataModule.kt @@ -14,10 +14,11 @@ import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.mapWithStringKeyTypes -import com.tangem.utils.coroutines.AppCoroutineScope +import com.tangem.domain.quotes.GetCurrencyUSDQuoteUseCase import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteUpdater +import com.tangem.utils.coroutines.AppCoroutineScope import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -72,4 +73,10 @@ internal object QuotesDataModule { coroutineScope = coroutineScope, ) } + + @Singleton + @Provides + fun provideGetCurrencyUSDQuoteUseCase(quotesRepository: QuotesRepository): GetCurrencyUSDQuoteUseCase { + return GetCurrencyUSDQuoteUseCase(quotesRepository) + } } \ No newline at end of file diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt index 6b1099fb1a..768a1fba48 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt @@ -55,7 +55,7 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor( val response = quotesFetcher.fetch( fiatCurrencyId = appCurrencyId, currenciesIds = replacementIdsResult.idsForRequest, - fields = setOf(Field.PRICE, Field.PRICE_CHANGE_24H), + fields = setOf(Field.PRICE, Field.PRICE_CHANGE_24H, Field.PRICE_USD), ) .getOrElse { error("Cause: $it") } diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt b/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt index d16b775518..6a044e7be3 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/repository/DefaultQuotesRepository.kt @@ -31,4 +31,8 @@ internal class DefaultQuotesRepository( ?: QuoteStatus(rawCurrencyId = currencyId) } } + + override suspend fun getCurrencyUSDQuote(currencyId: CryptoCurrency.RawID): QuoteStatus? { + return getMultiQuoteSyncOrNull(currenciesIds = setOf(currencyId)).firstOrNull() + } } \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/converter/QuoteStatusConverterTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/converter/QuoteStatusConverterTest.kt index f6653a4966..a22d7c9513 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/converter/QuoteStatusConverterTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/converter/QuoteStatusConverterTest.kt @@ -55,6 +55,7 @@ internal class QuoteStatusConverterTest { priceChange24h = null, priceChange1w = null, priceChange30d = null, + priceUsd = null, ), ), expected = QuoteStatus( @@ -62,6 +63,7 @@ internal class QuoteStatusConverterTest { value = QuoteStatus.Data( source = StatusSource.ACTUAL, fiatRate = BigDecimal.ZERO, + fiatRateUSD = BigDecimal.ZERO, priceChange = BigDecimal("0.00"), ), ), @@ -74,6 +76,7 @@ internal class QuoteStatusConverterTest { priceChange24h = null, priceChange1w = BigDecimal.ZERO, priceChange30d = BigDecimal.ZERO, + priceUsd = null, ), ), expected = QuoteStatus( @@ -81,6 +84,7 @@ internal class QuoteStatusConverterTest { value = QuoteStatus.Data( source = StatusSource.ACTUAL, fiatRate = BigDecimal.ZERO, + fiatRateUSD = BigDecimal.ZERO, priceChange = BigDecimal("0.00"), ), ), @@ -93,6 +97,7 @@ internal class QuoteStatusConverterTest { priceChange24h = BigDecimal.ONE, priceChange1w = null, priceChange30d = null, + priceUsd = BigDecimal.ONE ), ), expected = QuoteStatus( @@ -100,6 +105,7 @@ internal class QuoteStatusConverterTest { value = QuoteStatus.Data( source = StatusSource.ACTUAL, fiatRate = BigDecimal.ONE, + fiatRateUSD = BigDecimal.ONE, priceChange = BigDecimal("0.01"), ), ), diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt index 34c1f0af71..bc4e500fc0 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcherTest.kt @@ -240,6 +240,6 @@ internal class DefaultMultiQuoteStatusFetcherTest { ), ) - val fields = setOf(QuotesFetcher.Field.PRICE, QuotesFetcher.Field.PRICE_CHANGE_24H) + val fields = setOf(QuotesFetcher.Field.PRICE, QuotesFetcher.Field.PRICE_CHANGE_24H, QuotesFetcher.Field.PRICE_USD) } } \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/repository/DefaultQuotesRepositoryTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/repository/DefaultQuotesRepositoryTest.kt index ee674d4e2a..ea9203adf3 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/repository/DefaultQuotesRepositoryTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/repository/DefaultQuotesRepositoryTest.kt @@ -43,6 +43,7 @@ internal class DefaultQuotesRepositoryTest { value = QuoteStatus.Data( source = StatusSource.ACTUAL, fiatRate = BigDecimal.ZERO, + fiatRateUSD = BigDecimal.ZERO, priceChange = BigDecimal.ZERO, ), ) @@ -100,9 +101,75 @@ internal class DefaultQuotesRepositoryTest { ) } + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetCurrencyUSDQuote { + + private val btcRawId = CryptoCurrency.RawID(value = "BTC") + private val ethRawId = CryptoCurrency.RawID(value = "ETH") + + private val ethQuote = QuoteStatus( + rawCurrencyId = ethRawId, + value = QuoteStatus.Data( + source = StatusSource.ACTUAL, + fiatRate = BigDecimal.ZERO, + fiatRateUSD = BigDecimal.ZERO, + priceChange = BigDecimal.ZERO, + ), + ) + + @ParameterizedTest + @ProvideTestModels + fun getCurrencyUSDQuote(model: GetCurrencyUSDQuoteModel) = runTest { + // Arrange + coEvery { quotesStatusesStore.getAllSyncOrNull() } returns model.initialStore + + // Act + val actual = repository.getCurrencyUSDQuote(currencyId = model.currencyId) + + // Assert + val expected = model.expected + Truth.assertThat(actual).isEqualTo(expected) + } + + private fun provideTestModels() = listOf( + GetCurrencyUSDQuoteModel( + initialStore = null, + currencyId = ethRawId, + expected = QuoteStatus(rawCurrencyId = ethRawId), + ), + GetCurrencyUSDQuoteModel( + initialStore = emptySet(), + currencyId = ethRawId, + expected = QuoteStatus(rawCurrencyId = ethRawId), + ), + GetCurrencyUSDQuoteModel( + initialStore = setOf(ethQuote), + currencyId = ethRawId, + expected = ethQuote, + ), + GetCurrencyUSDQuoteModel( + initialStore = setOf(QuoteStatus(rawCurrencyId = btcRawId)), + currencyId = ethRawId, + expected = QuoteStatus(rawCurrencyId = ethRawId), + ), + GetCurrencyUSDQuoteModel( + initialStore = setOf(ethQuote, QuoteStatus(rawCurrencyId = btcRawId)), + currencyId = ethRawId, + expected = ethQuote, + ), + ) + } + data class GetMultiQuoteSyncOrNullModel( val initialStore: Set?, val currencyIds: Set, val expected: Set?, ) + + data class GetCurrencyUSDQuoteModel( + val initialStore: Set?, + val currencyId: CryptoCurrency.RawID, + val expected: QuoteStatus?, + ) } \ No newline at end of file diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt index 3360dfe25b..486788d947 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/single/DefaultSingleQuoteStatusProducerTest.kt @@ -83,6 +83,7 @@ internal class DefaultSingleQuoteStatusProducerTest { value = QuoteStatus.Data( fiatRate = BigDecimal.ONE, priceChange = BigDecimal.ZERO, + fiatRateUSD = BigDecimal.ZERO, source = StatusSource.ACTUAL, ), ) @@ -129,6 +130,7 @@ internal class DefaultSingleQuoteStatusProducerTest { rawCurrencyId = params.rawCurrencyId, value = QuoteStatus.Data( fiatRate = BigDecimal.ONE, + fiatRateUSD = BigDecimal.ZERO, priceChange = BigDecimal.ZERO, source = StatusSource.ACTUAL, ), diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/quote/QuoteStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/quote/QuoteStatus.kt index 1aad20046d..74f40ab8c6 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/quote/QuoteStatus.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/quote/QuoteStatus.kt @@ -40,11 +40,13 @@ data class QuoteStatus(val rawCurrencyId: CryptoCurrency.RawID, val value: Value * * @property source status source * @property fiatRate the current fiat exchange rate for the cryptocurrency + * @property fiatRateUSD the current fiat exchange rate in USD for the cryptocurrency * @property priceChange the price change for the cryptocurrency */ data class Data( override val source: StatusSource, val fiatRate: BigDecimal, + val fiatRateUSD: BigDecimal, val priceChange: BigDecimal, ) : Value } diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/GetCurrencyUSDQuoteUseCase.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/GetCurrencyUSDQuoteUseCase.kt new file mode 100644 index 0000000000..9271249145 --- /dev/null +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/GetCurrencyUSDQuoteUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.quotes + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus +import java.math.BigDecimal + +/** + * Get currency USD quote use case + */ +class GetCurrencyUSDQuoteUseCase( + private val quotesRepository: QuotesRepository, +) { + + /** Get quote by [currencyId] synchronously or null */ + suspend operator fun invoke(currencyId: CryptoCurrency.RawID): BigDecimal? { + val value = quotesRepository.getCurrencyUSDQuote(currencyId)?.value + + return if (value is QuoteStatus.Data) { + value.fiatRateUSD + } else { + null + } + } +} \ No newline at end of file diff --git a/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepository.kt b/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepository.kt index 9431c2b3f2..4744001c8b 100644 --- a/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepository.kt +++ b/domain/quotes/src/main/java/com/tangem/domain/quotes/QuotesRepository.kt @@ -12,4 +12,7 @@ interface QuotesRepository { /** Get quotes by [currenciesIds] synchronously or null */ suspend fun getMultiQuoteSyncOrNull(currenciesIds: Set): Set? + + /** Get quote by [currencyId] synchronously or null */ + suspend fun getCurrencyUSDQuote(currencyId: CryptoCurrency.RawID): QuoteStatus? } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt index 12b9de0cbc..081ad0fbf2 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt @@ -13,6 +13,7 @@ internal object MockQuotes { rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!, value = QuoteStatus.Data( fiatRate = BigDecimal("1.23"), + fiatRateUSD = BigDecimal("1.23"), priceChange = BigDecimal("0.01"), source = StatusSource.ACTUAL, ), @@ -22,6 +23,7 @@ internal object MockQuotes { rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!, value = QuoteStatus.Data( fiatRate = BigDecimal("2.34"), + fiatRateUSD = BigDecimal("2.34"), priceChange = BigDecimal("-0.02"), source = StatusSource.ACTUAL, ), @@ -31,6 +33,7 @@ internal object MockQuotes { rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!, value = QuoteStatus.Data( fiatRate = BigDecimal("3.45"), + fiatRateUSD = BigDecimal("3.45"), priceChange = BigDecimal("0.03"), source = StatusSource.ACTUAL, ), @@ -40,6 +43,7 @@ internal object MockQuotes { rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!, value = QuoteStatus.Data( fiatRate = BigDecimal("4.56"), + fiatRateUSD = BigDecimal("4.56"), priceChange = BigDecimal("-0.04"), source = StatusSource.ACTUAL, ), @@ -49,6 +53,7 @@ internal object MockQuotes { rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!, value = QuoteStatus.Data( fiatRate = BigDecimal("5.67"), + fiatRateUSD = BigDecimal("5.67"), priceChange = BigDecimal("0.05"), source = StatusSource.ACTUAL, ), @@ -58,6 +63,7 @@ internal object MockQuotes { rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!, value = QuoteStatus.Data( fiatRate = BigDecimal("6.78"), + fiatRateUSD = BigDecimal("6.78"), priceChange = BigDecimal("-0.06"), source = StatusSource.ACTUAL, ), @@ -67,6 +73,7 @@ internal object MockQuotes { rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!, value = QuoteStatus.Data( fiatRate = BigDecimal("7.89"), + fiatRateUSD = BigDecimal("7.89"), priceChange = BigDecimal("0.07"), source = StatusSource.ACTUAL, ), @@ -76,6 +83,7 @@ internal object MockQuotes { rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!, value = QuoteStatus.Data( fiatRate = BigDecimal("8.90"), + fiatRateUSD = BigDecimal("8.90"), priceChange = BigDecimal("-0.08"), source = StatusSource.ACTUAL, ), @@ -85,6 +93,7 @@ internal object MockQuotes { rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!, value = QuoteStatus.Data( fiatRate = BigDecimal("9.01"), + fiatRateUSD = BigDecimal("9.01"), priceChange = BigDecimal("0.09"), source = StatusSource.ACTUAL, ), @@ -94,6 +103,7 @@ internal object MockQuotes { rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!, value = QuoteStatus.Data( fiatRate = BigDecimal("10.12"), + fiatRateUSD = BigDecimal("10.12"), priceChange = BigDecimal("-0.10"), source = StatusSource.ACTUAL, ), diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactoryTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactoryTest.kt index f8a1d282a2..966dada81e 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactoryTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/operations/CryptoCurrencyStatusFactoryTest.kt @@ -44,6 +44,7 @@ class CryptoCurrencyStatusFactoryTest { private val fullQuote = QuoteStatus.Data( fiatRate = 1800.0.toBigDecimal(), + fiatRateUSD = 1800.0.toBigDecimal(), priceChange = (-2.5).toBigDecimal(), source = StatusSource.ACTUAL, ) diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt index 423c697e4f..c26f6ba501 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt @@ -83,6 +83,7 @@ class YieldSupplyMinAmountUseCaseTest { value = QuoteStatus.Data( source = StatusSource.ACTUAL, fiatRate = nativeFiatRate, + fiatRateUSD = nativeFiatRate, priceChange = BigDecimal("0.09000000000000007"), ), ), diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt index 711754e926..f36d85dfaa 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt @@ -79,6 +79,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest { value = QuoteStatus.Data( source = StatusSource.ACTUAL, fiatRate = nativeFiatRate, + fiatRateUSD = nativeFiatRate, priceChange = BigDecimal.ZERO, ), ), @@ -130,6 +131,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest { value = QuoteStatus.Data( source = StatusSource.ACTUAL, fiatRate = nativeFiatRate, + fiatRateUSD = nativeFiatRate, priceChange = BigDecimal.ZERO, ), ), @@ -256,6 +258,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest { value = QuoteStatus.Data( source = StatusSource.ACTUAL, fiatRate = BigDecimal.ZERO, // non-positive + fiatRateUSD = BigDecimal.ZERO, priceChange = BigDecimal.ZERO, ), ), From 30555b2c0b2b5816239fcc77a466bcb05b786852 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Mar 2026 12:23:21 +0500 Subject: [PATCH 28/75] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 2 +- core/res/src/main/res/values-ja/strings.xml | 2 +- .../src/main/res/values-pt-rBR/strings.xml | 21 +++++ .../src/main/res/values-zh-rCN/strings.xml | 2 +- core/res/src/main/res/values/strings.xml | 5 +- .../core/ui/test/SwapTokenScreenTestTags.kt | 1 - .../feature/swap/domain/SwapInteractorImpl.kt | 77 +++++++++++----- .../swap/domain/models/ui/SwapState.kt | 31 +++++-- .../tangem/feature/swap/model/SwapModel.kt | 2 +- .../swap/model/SwapNotificationsFactory.kt | 13 +++ .../feature/swap/models/SwapStateHolder.kt | 6 +- .../swap/models/states/SwapNotificationUM.kt | 11 +++ .../tangem/feature/swap/ui/StateBuilder.kt | 91 +++++++++++-------- .../feature/swap/ui/SwapScreenContent.kt | 7 +- .../tangem/feature/swap/ui/TransactionCard.kt | 78 ++++++---------- 15 files changed, 222 insertions(+), 127 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 72e7efe111..5132421c98 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -694,7 +694,7 @@ Lege die Karte oder Ring an Deine Wallet ist eingerichtet und einsatzbereit! Wallet erfolgreich importiert - Wiederherstellung %d% + Wiederherstellung %d%% Du hast deine biometrischen Daten aktualisiert, scanne deine Karte oder Ring, um einzutreten Dein Guthaben sollte höher sein als der Gebührenwert, um eine Überweisung zu tätigen Unzureichende Mittel diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index e4a52817fd..6dadf3d9d5 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -689,7 +689,7 @@ カードまたはリングをタップ ウォレットは同期され、準備ができています。\n一部のトークンが見つかりませんか? ウォレットのインポートが完了しました - %d%を復元中 + %d%%を復元中 生体認証を更新しました。カードまたはリングをスキャンして入ってください。 送金するには、残高が手数料額より高くなければなりません。 残高不足 diff --git a/core/res/src/main/res/values-pt-rBR/strings.xml b/core/res/src/main/res/values-pt-rBR/strings.xml index de83ee0e8f..c6642f4bfa 100644 --- a/core/res/src/main/res/values-pt-rBR/strings.xml +++ b/core/res/src/main/res/values-pt-rBR/strings.xml @@ -306,6 +306,7 @@ hora Importar Em andamento + Saldo insuficiente Mais tarde Saber mais %1$s falta @@ -692,6 +693,9 @@ Toque para digitalizar Toque para assinar Toque no Cartão ou no Anel + Sua carteira está sincronizada e pronta. \n Faltam alguns tokens? + Carteira importada com sucesso. + Restaurando %d%% Você atualizou seus dados biométricos. Escaneie seu cartão ou toque a campainha para entrar. Seu saldo deve ser superior ao valor da taxa para efetuar uma transferência. Saldo insuficiente @@ -752,6 +756,7 @@ Resultado Veja tokens com capitalização de mercado inferior a 100 mil dólares. Mostrar tokens + Criptomoedas, notícias e muito mais Nenhum resultado Selecione a rede Selecione a carteira @@ -824,9 +829,11 @@ Posição no ranking de criptomoedas entre todas as moedas com base na capitalização de mercado. Posicionamento de mercado Fornecimento máximo + Circulação e Fornecimento Máximo O número máximo de moedas ou tokens que podem existir para uma determinada criptomoeda. Fornecimento máximo Métricas + Sem limite Links oficiais Desempenho de preço Repositório @@ -1101,6 +1108,12 @@ Configurações Você não concedeu acesso à sua câmera. Acesso à câmera negado + O token solicitado não foi adicionado à sua carteira. Adicione-o e tente novamente. + Token não adicionado + Desculpe, este código QR não pôde ser reconhecido. + Código QR não reconhecido + Esta rede não é compatível com nenhum dos tokens adicionados. Adicione um token compatível para enviar criptomoedas. + Nenhum token compatível encontrado Não é necessário memorando %1$s (%2$s) sobre %3$s rede %1$s sobre %2$s rede @@ -1246,6 +1259,11 @@ Limitação de transação Opcional Alinhe o seu código QR com o quadrado para escaneá-lo. Certifique-se de escanear corretamente. %s endereço de rede. + Ao utilizar uma taxa fixa, o valor que você recebe fica garantido no momento da troca. Isso protege você de variações de preço durante a transação. + Taxa fixa + Uma taxa flutuante significa que o valor final que você recebe pode variar ligeiramente com base nas condições de mercado entre o momento em que você inicia e conclui a operação de swap. + Taxa flutuante + A taxa é fixa. Recente Destinatário Endereço inválido @@ -1289,6 +1307,7 @@ O destinatário recebe %s Tem certeza de que deseja cancelar a conversão? Seus dados anteriores serão apagados. Remover conversão + Algo deu errado. Tente novamente. Enviar com troca Transação enviada Prepare-se para escanear o Cartão ou Anel que deseja configurar. @@ -1482,6 +1501,7 @@ Trocar essa quantidade de tokens selecionados causará um impacto significativo no preço e reduzirá seu resultado. Alto impacto nos preços Fundos insuficientes + Não há fundos suficientes para concluir esta transação. Reduza o valor a receber ou adicione mais fundos. Conceder permissão Trocar Trocar... @@ -1648,6 +1668,7 @@ Token em %%imagem%% %1$s rede O %1$s (%2$sO token ) é a principal moeda da plataforma. %3$s rede e não pode ser ocultada enquanto você tiver outros tokens dessa rede na lista Não foi possível ocultar %s + N/A Mostrar código QR Troque este token por outro em %1$s taxas de serviço a partir de fevereiro %2$s-%3$s. Trocar com Changelly, %s tarifas diff --git a/core/res/src/main/res/values-zh-rCN/strings.xml b/core/res/src/main/res/values-zh-rCN/strings.xml index be6b9c60c8..9a07214403 100644 --- a/core/res/src/main/res/values-zh-rCN/strings.xml +++ b/core/res/src/main/res/values-zh-rCN/strings.xml @@ -6,7 +6,7 @@ 解鎖 您的錢包已同步並準備就緒。\n缺少某些代幣? 錢包導入成功 - 恢復%d% + 恢復%d%% diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 1823294875..d4a0a1480f 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -696,7 +696,7 @@ Tap the card or ring Your wallet is synced and ready.\nSome tokens missing? Wallet successfully imported - Restoring %d% + Restoring %d%% You have updated biometrics, scan your card or ring to enter Your balance should be higher than the fee value to make a transfer Not enough balance @@ -1500,6 +1500,7 @@ Fee estimation error. Please send feedback to support. You swap Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. + You may receive significantly less due to low liquidity. Try a smaller amount or another provider. High price impact Insufficient funds Not enough funds to complete this transaction. Reduce the amount to receive or add more funds. @@ -1509,6 +1510,8 @@ You receive Choose token not available + Not enough liquidity for this trade.\nReduce the amount or choose another provider. + Trade too large We would be happy to receive your feedback Tangem Pay is now in beta Card frozen diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt index 726604b2b1..2a200563fa 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt @@ -16,7 +16,6 @@ object SwapTokenScreenTestTags { const val TOKEN_ICON = "SWAP_TOKEN_SCREEN_TOKEN_ICON" const val SELECT_TOKEN_ICON = "SWAP_TOKEN_SCREEN_SELECT_TOKEN_ICON" const val RECEIVE_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT" - const val RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT" const val RECEIVE_FIAT_AMOUNT_INFORMATION_ICON = "SWAP_TOKEN_SCREEN_PRICE_IMPACT_INFORMATION_ICON" const val SWAP_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_SWAP_FIAT_AMOUNT" } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 14b10c2aad..2a22cca812 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -17,6 +17,7 @@ import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldS import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer @@ -67,11 +68,12 @@ import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.* import com.tangem.lib.crypto.BlockchainUtils.SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.extensions.orZero +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.coroutineScope -import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.math.BigInteger import java.math.RoundingMode @@ -1289,12 +1291,14 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun createEmptyAmountState(): SwapState { val appCurrency = getSelectedAppCurrencyUseCase.unwrap() return SwapState.EmptyAmountState( - zeroAmountEquivalent = BigDecimal.ZERO.format { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - }, + zeroAmountEquivalent = stringReference( + BigDecimal.ZERO.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + ), ) } @@ -1523,7 +1527,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val rates = getQuotes(fromToken.currency.id) val fromTokenSwapInfo = TokenSwapInfo( tokenAmount = amount, - amountFiat = rates[fromToken.currency.id]?.multiply(amount.value) + amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value) ?: BigDecimal.ZERO, cryptoCurrencyStatus = fromToken, account = fromAccount, @@ -1687,7 +1691,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val rates = getQuotes(feeCurrencyId) return rates[feeCurrencyId]?.let { rate -> fees.map { fee -> - rate.multiply(fee).format { + rate.fiatRate.multiply(fee).format { fiat( fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, @@ -1855,7 +1859,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val rates = getQuotes(fromToken.currency.id) val fromTokenSwapInfo = TokenSwapInfo( tokenAmount = amount, - amountFiat = rates[fromToken.currency.id]?.multiply(amount.value) + amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value) ?: BigDecimal.ZERO, cryptoCurrencyStatus = fromToken, account = fromAccount, @@ -1963,21 +1967,21 @@ internal class SwapInteractorImpl @AssistedInject constructor( tokenAmount = fromTokenAmount, account = fromAccount, cryptoCurrencyStatus = fromTokenStatus, - amountFiat = rates[fromToken.id]?.multiply(fromTokenAmount.value) + amountFiat = rates[fromToken.id]?.fiatRate?.multiply(fromTokenAmount.value) ?: BigDecimal.ZERO, ), toTokenInfo = TokenSwapInfo( tokenAmount = toTokenAmount, cryptoCurrencyStatus = toTokenStatus, account = toAccount, - amountFiat = rates[toToken.id]?.multiply(toTokenAmount.value) + amountFiat = rates[toToken.id]?.fiatRate?.multiply(toTokenAmount.value) ?: BigDecimal.ZERO, ), priceImpact = calculatePriceImpact( fromTokenAmount = fromTokenAmount.value, - fromRate = rates[fromToken.id]?.toDouble() ?: 0.0, + fromQuoteStatus = rates[fromToken.id], toTokenAmount = toTokenAmount.value, - toRate = rates[toToken.id]?.toDouble() ?: 0.0, + toRate = rates[toToken.id]?.fiatRate, ), swapDataModel = swapData, swapProvider = provider, @@ -2446,17 +2450,42 @@ internal class SwapInteractorImpl @AssistedInject constructor( private fun calculatePriceImpact( fromTokenAmount: BigDecimal, - fromRate: Double, + fromQuoteStatus: QuoteStatus.Data?, toTokenAmount: BigDecimal, - toRate: Double, + toRate: BigDecimal?, ): PriceImpact { - val fromTokenFiatValue = fromTokenAmount.multiply(fromRate.toBigDecimal()) - val toTokenFiatValue = toTokenAmount.multiply(toRate.toBigDecimal()) - val value = (BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP)).toFloat() - return PriceImpact.Value(value) + if (fromQuoteStatus == null) return PriceImpact.Empty + + val fromTokenFiatValue = fromTokenAmount.multiply(fromQuoteStatus.fiatRate) + val toTokenFiatValue = toTokenAmount.multiply(toRate.orZero()) + val value = BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP) + + val fromAmountUSD = if (fromQuoteStatus.fiatRateUSD != BigDecimal.ZERO) { + fromTokenAmount.multiply(fromQuoteStatus.fiatRateUSD) + } else { + PRICE_IMPACT_AMOUNT_MIN_THRESHOLD + } + + val amountSignificance = when { + fromAmountUSD < PRICE_IMPACT_AMOUNT_MIN_THRESHOLD -> PriceImpact.AmountSignificance.LOW + fromAmountUSD > PRICE_IMPACT_AMOUNT_MAX_THRESHOLD -> PriceImpact.AmountSignificance.HIGH + else -> PriceImpact.AmountSignificance.MEDIUM + } + + val type = when { + value < PRICE_IMPACT_LOW_THRESHOLD -> PriceImpact.Type.LOW + value in PRICE_IMPACT_LOW_THRESHOLD..PRICE_IMPACT_HIGH_THRESHOLD -> PriceImpact.Type.MEDIUM + else -> PriceImpact.Type.HIGH + } + + return PriceImpact( + value = value, + amountSignificance = amountSignificance, + type = type, + ) } - private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map { + private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map { val set = ids.mapNotNullTo(destination = hashSetOf(), transform = CryptoCurrency.ID::rawCurrencyId) .getQuotesOrEmpty() @@ -2465,7 +2494,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val found = set.find { it.rawCurrencyId == id.rawCurrencyId && it.value is QuoteStatus.Data } ?: return@mapNotNull null - id to (found.value as QuoteStatus.Data).fiatRate + id to found.value as QuoteStatus.Data } .toMap() } @@ -2523,6 +2552,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( companion object { private const val INCREASE_GAS_LIMIT_FOR_DEX = 112 // 12% private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5% + private val PRICE_IMPACT_AMOUNT_MIN_THRESHOLD = 50.toBigDecimal() // in USD + private val PRICE_IMPACT_AMOUNT_MAX_THRESHOLD = 5000.toBigDecimal() // in USD + private val PRICE_IMPACT_LOW_THRESHOLD = 0.1.toBigDecimal() // 10% + private val PRICE_IMPACT_HIGH_THRESHOLD = 0.5.toBigDecimal() // 50% private const val INFINITY_SYMBOL = "∞" } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index c735c821d4..fa1c9bdf73 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -1,6 +1,8 @@ package com.tangem.feature.swap.domain.models.ui +import androidx.compose.runtime.Immutable import com.tangem.blockchain.common.transaction.Fee +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck @@ -36,7 +38,7 @@ sealed interface SwapState { val swapProvider: SwapProvider, ) : SwapState - data class EmptyAmountState(val zeroAmountEquivalent: String) : SwapState + data class EmptyAmountState(val zeroAmountEquivalent: TextReference) : SwapState data class SwapError( val fromTokenInfo: TokenSwapInfo, @@ -45,18 +47,31 @@ sealed interface SwapState { ) : SwapState } -sealed class PriceImpact { +@Immutable +data class PriceImpact( + val value: BigDecimal, + val amountSignificance: AmountSignificance, + val type: Type, +) { - abstract val value: Float + enum class Type { + NONE, LOW, MEDIUM, HIGH + } - data class Empty(override val value: Float = 0f) : PriceImpact() + enum class AmountSignificance { + LOW, MEDIUM, HIGH + } - data class Value(override val value: Float) : PriceImpact() - - fun getIntPercentValue() = (value * HUNDRED_PERCENTS).toInt() + fun shouldDisableButton(): Boolean { + return type == Type.HIGH && amountSignificance == AmountSignificance.HIGH + } companion object { - private const val HUNDRED_PERCENTS = 100 + val Empty = PriceImpact( + value = BigDecimal.ZERO, + amountSignificance = AmountSignificance.LOW, + type = Type.NONE, + ) } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 789816698c..53cf08c449 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -1700,7 +1700,7 @@ internal class SwapModel @Inject constructor( onReceiveCardWarningClick = { val selectedProvider = dataState.selectedProvider ?: return@UiActions val currencySymbol = dataState.toCryptoCurrency?.currency?.symbol ?: return@UiActions - val isPriceImpact = uiState.priceImpact is PriceImpact.Value + val isPriceImpact = uiState.priceImpact.type != PriceImpact.Type.NONE showSwapInfoAlert(isPriceImpact, currencySymbol, selectedProvider) }, onLinkClick = urlOpener::openUrl, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 5bf13c6e4d..6586b11550 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -118,6 +118,7 @@ internal class SwapNotificationsFactory( maybeAddNetworkFeeCoverageWarning(quoteModel, selectedFeeType) maybeAddUnableCoverFeeWarning(quoteModel, fromToken, hideFee) maybeAddTransactionInProgressWarning(quoteModel) + maybeAddPriceImpactNotification(quoteModel.priceImpact) } return warnings.toPersistentList() } @@ -143,6 +144,18 @@ internal class SwapNotificationsFactory( } } + private fun MutableList.maybeAddPriceImpactNotification(priceImpact: PriceImpact) { + if (priceImpact.amountSignificance == PriceImpact.AmountSignificance.LOW) return + + val notification = when (priceImpact.type) { + PriceImpact.Type.HIGH -> SwapNotificationUM.Warning.TradeTooHigh + PriceImpact.Type.MEDIUM -> SwapNotificationUM.Warning.HighPriceImpact + else -> return + } + + add(notification) + } + @Suppress("LongMethod") private fun MutableList.maybeAddDomainWarnings( quoteModel: SwapState.QuotesLoadedState, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index f3d9a7bc7d..0afbd2da4b 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.models import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.ui.account.AccountTitleUM import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState @@ -47,7 +48,7 @@ sealed class SwapCardState { data class SwapCardData( @DrawableRes val networkIconRes: Int?, val type: TransactionCardType, - val amountEquivalent: String?, + val amountEquivalent: TextReference?, val token: CryptoCurrencyStatus?, val coinId: String?, val amountTextFieldValue: TextFieldValue?, @@ -61,7 +62,7 @@ sealed class SwapCardState { data class Empty( val type: TransactionCardType, - val amountEquivalent: String?, + val amountEquivalent: TextReference?, val amountTextFieldValue: TextFieldValue?, val canSelectAnotherToken: Boolean = false, ) : SwapCardState() @@ -75,6 +76,7 @@ data class SwapButton( val onClick: () -> Unit, ) +@Immutable sealed interface TransactionCardType { val accountTitleUM: AccountTitleUM? diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt index e158fd5f3b..96a0293a82 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt @@ -194,6 +194,17 @@ internal object SwapNotificationUM { onClick = onConfirmClick, ), ) + + data object HighPriceImpact : Warning( + title = resourceReference(R.string.swapping_high_price_impact_title), + subtitle = resourceReference(R.string.swapping_high_price_impact_text), + ) + + data object TradeTooHigh : Warning( + title = resourceReference(R.string.swapping_trade_too_large_title), + subtitle = resourceReference(R.string.swapping_trade_too_large_text), + iconResId = R.drawable.ic_alert_circle_24, + ) } sealed class Info( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index e65fefdf6c..6ba8b6a6e4 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.ui +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.ui.account.AccountTitleUM @@ -12,10 +13,8 @@ import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.* -import com.tangem.core.ui.format.bigdecimal.anyDecimals -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.* +import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency @@ -37,6 +36,7 @@ import com.tangem.feature.swap.models.states.* import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.utils.formatToUIRepresentation import com.tangem.utils.Provider +import com.tangem.utils.StringsSigns import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.StringsSigns.TILDE_SIGN import kotlinx.collections.immutable.ImmutableList @@ -127,7 +127,7 @@ internal class StateBuilder( onSuccess = actions.onSuccess, providerState = ProviderState.Empty(), shouldShowMaxAmount = false, - priceImpact = PriceImpact.Empty(), + priceImpact = PriceImpact.Empty, isInsufficientFunds = false, ) } @@ -141,7 +141,7 @@ internal class StateBuilder( sendCardData = SwapCardState.SwapCardData( type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable), amountTextFieldValue = null, - amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO), token = fromToken, tokenIconUrl = fromToken.currency.iconUrl, coinId = fromToken.currency.network.backendId, @@ -154,7 +154,7 @@ internal class StateBuilder( ), receiveCardData = SwapCardState.Empty( type = TransactionCardType.ReadOnly(), - amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO), amountTextFieldValue = TextFieldValue( text = "0", ), @@ -169,7 +169,7 @@ internal class StateBuilder( onClick = { }, ), changeCardsButtonState = ChangeCardsButtonState.DISABLED, - priceImpact = PriceImpact.Empty(), + priceImpact = PriceImpact.Empty, ) } @@ -187,7 +187,7 @@ internal class StateBuilder( accountTitleUM = getFromCardAccountTitle(fromAccount), ), amountTextFieldValue = null, - amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO), token = fromToken, tokenIconUrl = fromToken.currency.iconUrl, coinId = fromToken.currency.network.backendId, @@ -205,7 +205,7 @@ internal class StateBuilder( amountTextFieldValue = TextFieldValue( text = "0", ), - amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO), token = toToken, tokenIconUrl = toToken.currency.iconUrl, coinId = toToken.currency.network.backendId, @@ -226,7 +226,7 @@ internal class StateBuilder( ), changeCardsButtonState = ChangeCardsButtonState.DISABLED, providerState = ProviderState.Empty(), - priceImpact = PriceImpact.Empty(), + priceImpact = PriceImpact.Empty, ) } @@ -294,7 +294,7 @@ internal class StateBuilder( providerState = ProviderState.Loading(), permissionState = uiStateHolder.permissionState, changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, - priceImpact = PriceImpact.Empty(), + priceImpact = PriceImpact.Empty, shouldShowMaxAmount = shouldShowMaxAmount(fromToken, toToken), ) } @@ -360,6 +360,7 @@ internal class StateBuilder( ) } } + val priceImpact = quoteModel.priceImpact return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( type = sendInput, @@ -386,7 +387,26 @@ internal class StateBuilder( .formatToUIRepresentation() .appendApproximateSign(), ), - amountEquivalent = getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat), + amountEquivalent = combinedReference( + getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat), + stringReference(StringsSigns.WHITE_SPACE), + styledStringReference( + priceImpact.value.format { + percent(withoutSign = false) + }, + spanStyleReference = { + SpanStyle( + color = when (priceImpact.type) { + PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning + PriceImpact.Type.MEDIUM -> TangemTheme.colors.text.attention + PriceImpact.Type.LOW, + PriceImpact.Type.NONE, + -> TangemTheme.colors.text.tertiary + }, + ) + }, + ), + ), token = toCurrencyStatus, tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, coinId = toCurrencyStatus.currency.network.backendId, @@ -410,7 +430,7 @@ internal class StateBuilder( fee = feeState, swapButton = SwapButton( walletInteractionIcon = walletInterationIcon(userWalletProvider()), - isEnabled = getSwapButtonEnabled(notifications), + isEnabled = getSwapButtonEnabled(notifications, priceImpact), isHoldToConfirm = isHoldToConfirmEnabled, onClick = actions.onSwapClick, ), @@ -424,11 +444,7 @@ internal class StateBuilder( onProviderClick = actions.onProviderClick, needApplyFCARestrictions = needApplyFCARestrictions, ), - priceImpact = if (quoteModel.priceImpact.value > PRICE_IMPACT_THRESHOLD) { - quoteModel.priceImpact - } else { - PriceImpact.Empty() - }, + priceImpact = quoteModel.priceImpact, tosState = createTosState(swapProvider), shouldShowMaxAmount = shouldShowMaxAmount(fromToken, toCurrencyStatus.currency), ) @@ -462,7 +478,7 @@ internal class StateBuilder( quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included } - private fun getSwapButtonEnabled(notifications: ImmutableList): Boolean { + private fun getSwapButtonEnabled(notifications: ImmutableList, priceImpact: PriceImpact): Boolean { return notifications.none { notification -> notification is SwapNotificationUM.Error || notification is NotificationUM.Error || notification is SwapNotificationUM.Warning.ExpressError || @@ -471,7 +487,7 @@ internal class StateBuilder( notification is SwapNotificationUM.Warning.SwapNotSupported || notification is SwapNotificationUM.Warning.NeedReserveToCreateAccount || notification is SwapNotificationUM.Info.PermissionNeeded - } + } && !priceImpact.shouldDisableButton() } @Suppress("LongParameterList") @@ -510,7 +526,7 @@ internal class StateBuilder( amountTextFieldValue = TextFieldValue( text = "0", ), - amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO), token = toToken, tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, coinId = toToken.currency.network.backendId, @@ -523,7 +539,7 @@ internal class StateBuilder( ) } ?: SwapCardState.Empty( type = type, - amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO), amountTextFieldValue = TextFieldValue( text = "0", ), @@ -546,7 +562,7 @@ internal class StateBuilder( ), changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), providerState = providerState, - priceImpact = PriceImpact.Empty(), + priceImpact = PriceImpact.Empty, tosState = createTosState(swapProvider), ) } @@ -644,7 +660,7 @@ internal class StateBuilder( ), changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), providerState = ProviderState.Empty(), - priceImpact = PriceImpact.Empty(), + priceImpact = PriceImpact.Empty, ) } @@ -852,8 +868,8 @@ internal class StateBuilder( toTitle = getToCardAccountTitle(toAccount = dataState.toAccount), fromTokenAmount = stringReference(swapTransactionState.fromAmount.orEmpty()), toTokenAmount = stringReference(swapTransactionState.toAmount.orEmpty()), - fromTokenFiatAmount = stringReference(fromFiatAmount), - toTokenFiatAmount = stringReference(toFiatAmount), + fromTokenFiatAmount = fromFiatAmount, + toTokenFiatAmount = toFiatAmount, fromTokenIconState = iconStateConverter.convert(fromCryptoCurrency), toTokenIconState = iconStateConverter.convert(toCryptoCurrency), onExploreButtonClick = onExploreClick, @@ -892,8 +908,8 @@ internal class StateBuilder( toTitle = getToCardAccountTitle(toAccount = dataState.toAccount), fromTokenAmount = stringReference(swapTransactionState.fromAmount.orEmpty()), toTokenAmount = stringReference(swapTransactionState.toAmount.orEmpty()), - fromTokenFiatAmount = stringReference(fromFiatAmount), - toTokenFiatAmount = stringReference(toFiatAmount), + fromTokenFiatAmount = fromFiatAmount, + toTokenFiatAmount = toFiatAmount, fromTokenIconState = iconStateConverter.convert(fromCryptoCurrency), toTokenIconState = iconStateConverter.convert(toCryptoCurrency), onExploreButtonClick = onExploreClick, @@ -1267,15 +1283,17 @@ internal class StateBuilder( return amount.format { crypto(symbol, currency.decimals) } } - private fun getFormattedFiatAmount(amount: BigDecimal?): String { + private fun getFormattedFiatAmount(amount: BigDecimal?): TextReference { val appCurrency = appCurrencyProvider() - return amount.format { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - } + return stringReference( + amount.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + ) } private fun SwapAmount.getFormattedCryptoAmount(token: CryptoCurrency): String { @@ -1339,7 +1357,6 @@ internal class StateBuilder( const val ADDRESS_MIN_LENGTH = 11 const val ADDRESS_FIRST_PART_LENGTH = 7 const val ADDRESS_SECOND_PART_LENGTH = 4 - private const val PRICE_IMPACT_THRESHOLD = 0.1 private const val MAX_DECIMALS_TO_SHOW = 8 private const val IF_ZERO_DECIMALS_TO_SHOW = 2 private const val FEE_READ_MORE_URL_FIRST_PART = "https://tangem.com/" diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 2fdde2b04a..10d12201f0 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -346,6 +346,7 @@ private fun SwapNotifications(notifications: List) { Notification( config = notification.config, iconTint = when (notification) { + is SwapNotificationUM.Warning.TradeTooHigh -> TangemTheme.colors.icon.warning is SwapNotificationUM.Error.UnableToCoverFeeWarning, is NotificationUM.Error.TokenExceedsBalance, is NotificationUM.Error.ExceedsBalance, @@ -412,7 +413,7 @@ private val sendCard = SwapCardState.SwapCardData( accountTitleUM = null, ), amountTextFieldValue = TextFieldValue(), - amountEquivalent = "1 000 000", + amountEquivalent = stringReference("1 000 000"), tokenIconUrl = "", tokenCurrency = "DAI", isNotNativeToken = true, @@ -427,7 +428,7 @@ private val sendCard = SwapCardState.SwapCardData( private val receiveCard = SwapCardState.SwapCardData( type = TransactionCardType.ReadOnly(), amountTextFieldValue = TextFieldValue(), - amountEquivalent = "1 000 000", + amountEquivalent = stringReference("1 000 000"), tokenIconUrl = "", tokenCurrency = "DAI", isNotNativeToken = true, @@ -466,7 +467,7 @@ private val state = SwapStateHolder( permissionState = GiveTxPermissionState.InProgress, blockchainId = "POLYGON", providerState = ProviderState.Loading(), - priceImpact = PriceImpact.Empty(), + priceImpact = PriceImpact.Empty, shouldShowMaxAmount = true, isInsufficientFunds = false, onSuccess = {}, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index b29e4b8257..1886f3effd 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -26,9 +26,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -41,8 +39,7 @@ import com.tangem.common.ui.account.AccountTitleUM import com.tangem.common.ui.account.CryptoPortfolioIconConverter import com.tangem.core.ui.R import com.tangem.core.ui.components.* -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.SwapTokenScreenTestTags @@ -59,7 +56,7 @@ fun TransactionCard( balance: String, tokenIconUrl: String, tokenCurrency: String, - amountEquivalent: String?, + amountEquivalent: TextReference?, priceImpact: PriceImpact, textFieldValue: TextFieldValue?, modifier: Modifier = Modifier, @@ -129,7 +126,7 @@ fun TransactionCard( @Composable fun TransactionCardEmpty( type: TransactionCardType, - amountEquivalent: String?, + amountEquivalent: TextReference?, textFieldValue: TextFieldValue?, modifier: Modifier = Modifier, onChangeTokenClick: (() -> Unit)? = null, @@ -157,7 +154,7 @@ fun TransactionCardEmpty( type = type, amountEquivalent = amountEquivalent, textFieldValue = textFieldValue, - priceImpact = PriceImpact.Empty(), + priceImpact = PriceImpact.Empty, ) } @@ -250,7 +247,7 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie @Composable private fun Content( type: TransactionCardType, - amountEquivalent: String?, + amountEquivalent: TextReference?, priceImpact: PriceImpact, textFieldValue: TextFieldValue?, ) { @@ -320,27 +317,13 @@ private fun Content( modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20), verticalAlignment = Alignment.CenterVertically, ) { - if (priceImpact is PriceImpact.Value) { + AnimatedContent(targetState = amountEquivalent, label = "") { amount -> Text( - text = makePriceImpactBalanceWarning( - amountEquivalent, - priceImpact.getIntPercentValue(), - ), + text = amount.resolveAnnotatedReference(), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, - modifier = Modifier.testTag( - SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING, - ), + modifier = Modifier.testTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT), ) - } else { - AnimatedContent(targetState = amountEquivalent, label = "") { amount -> - Text( - text = amount, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - modifier = Modifier.testTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT), - ) - } } if (type.shouldShowWarning) { SpacerW4() @@ -353,10 +336,10 @@ private fun Content( Icon( painter = painterResource(id = R.drawable.ic_information_24), contentDescription = null, - tint = if (priceImpact is PriceImpact.Value) { - TangemTheme.colors.text.attention - } else { - TangemTheme.colors.text.tertiary + tint = when (priceImpact.type) { + PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning + PriceImpact.Type.MEDIUM -> TangemTheme.colors.text.attention + else -> TangemTheme.colors.text.tertiary }, modifier = Modifier .align(Alignment.CenterVertically) @@ -368,7 +351,7 @@ private fun Content( } else { AnimatedContent(targetState = amountEquivalent, label = "") { amount -> Text( - text = amount, + text = amount.resolveAnnotatedReference(), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, modifier = Modifier @@ -530,19 +513,6 @@ fun ChangeTokenSelector() { } } -@Composable -private fun makePriceImpactBalanceWarning(value: String, priceImpactPercents: Int): AnnotatedString { - val fullValue = "$value (-$priceImpactPercents%)" - return buildAnnotatedString { - append(fullValue) - addStyle( - style = SpanStyle(color = TangemTheme.colors.text.attention), - start = value.length, - end = fullValue.length, - ) - } -} - // region preview @Preview(widthDp = 328, heightDp = 116, showBackground = true) @@ -602,14 +572,14 @@ private fun TransactionCardPreview() { inputError = TransactionCardType.InputError.Empty, accountTitleUM = null, ), - amountEquivalent = "1 000 000", + amountEquivalent = stringReference("1 000 000"), tokenIconUrl = "", tokenCurrency = "DAI", networkIconRes = R.drawable.img_polygon_22, onChangeTokenClick = {}, balance = "123", textFieldValue = TextFieldValue(), - priceImpact = PriceImpact.Empty(), + priceImpact = PriceImpact.Empty, ) } @@ -625,14 +595,24 @@ private fun TransactionCardPreviewWithPriceImpact() { icon = CryptoPortfolioIconConverter.convert(CryptoPortfolioIcon.ofDefaultCustomAccount()), ), ), - amountEquivalent = "1 000 000", + amountEquivalent = combinedReference( + stringReference("1 000 000 $"), + styledStringReference( + " (-15%)", + { SpanStyle(color = TangemTheme.colors.text.attention) }, + ), + ), tokenIconUrl = "", tokenCurrency = "DAI", networkIconRes = R.drawable.img_polygon_22, onChangeTokenClick = {}, balance = "123", textFieldValue = TextFieldValue("1000000.0000000000000000000000000"), - priceImpact = PriceImpact.Value(0.15F), + priceImpact = PriceImpact( + value = 0.15F.toBigDecimal(), + type = PriceImpact.Type.MEDIUM, + amountSignificance = PriceImpact.AmountSignificance.HIGH, + ), ) } @@ -647,14 +627,14 @@ private fun TransactionCardPreviewWithoutPriceImpact() { icon = CryptoPortfolioIconConverter.convert(CryptoPortfolioIcon.ofDefaultCustomAccount()), ), ), - amountEquivalent = "1 000 000", + amountEquivalent = stringReference("1 000 000"), tokenIconUrl = "", tokenCurrency = "DAI", networkIconRes = R.drawable.img_polygon_22, onChangeTokenClick = {}, balance = "123", textFieldValue = TextFieldValue(), - priceImpact = PriceImpact.Empty(), + priceImpact = PriceImpact.Empty, ) } From 514eca0b00644d5ec378c4abd71e8fe18fa86e10 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Mar 2026 11:20:40 +0300 Subject: [PATCH 29/75] Updated on 2026-08-14 --- .../tokens/DefaultTokensFeatureToggles.kt | 4 ++-- .../assets/configs/feature_toggles_config.json | 2 +- core/res/src/main/res/values/strings.xml | 17 +++++++++++++++++ .../domain/tokens/TokensFeatureToggles.kt | 2 +- 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt index 70402f2f76..a223acdcac 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt @@ -8,6 +8,6 @@ internal class DefaultTokensFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : TokensFeatureToggles { - override val isMultiAddressUtxoEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.MULTI_ADDRESS_UTXO_ENABLED) + override val isDynamicAddressesEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.DYNAMIC_ADDRESSES_ENABLED) } \ No newline at end of file diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 5144c3702b..025b649a98 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -53,7 +53,7 @@ "version": "undefined" }, { - "name": "MULTI_ADDRESS_UTXO_ENABLED", + "name": "DYNAMIC_ADDRESSES_ENABLED", "version": "undefined" }, { diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index d4a0a1480f..53ed50688f 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -474,6 +474,18 @@ Sending assets in other networks will result in permanent loss. %s network Send funds using only + Dynamic addresses + Dynamic addresses enabled + Use a new address for each transaction to reduce traceability and improve on-chain privacy. + Enhanced Privacy + Easily receive funds in UTXO-based networks with automatic address generation — no manual address management required. + Seamless receiving + Enable Dynamic Addresses + Dynamic addresses create a new one each time for extra privacy — your total balance stays the same. + Dynamic Addresses cannot be enabled because some custom addresses/tokens use a modified derivation path, which doesn’t meet the required criteria. + Dynamic Addresses Unavailable + We can’t connect to the provider right now. Please try again later. + Service unavailable. Please try again. Best opportunities Clear filter The list is temporarily empty as it’s being refreshed. Check back in a moment. @@ -753,7 +765,10 @@ No data Market Pulse Quick actions + Clear all Search tokens + Recents + In your portfolio Result See tokens under 100k USD market cap Show tokens @@ -1115,6 +1130,8 @@ Unrecognized QR Code This network isn\'t supported by any of your added tokens. Add a supported token to send crypto. No supported tokens found + This QR code contains parameters that are not recognized: %s. Some payment details may be lost if you continue. + Unknown Parameters No memo required %1$s (%2$s) on %3$s network %1$s on %2$s network diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt index c281c5edd4..ea588e781c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt @@ -6,5 +6,5 @@ package com.tangem.domain.tokens [REDACTED_AUTHOR] */ interface TokensFeatureToggles { - val isMultiAddressUtxoEnabled: Boolean + val isDynamicAddressesEnabled: Boolean } \ No newline at end of file From 8829f6f12aafaa88a1ad46e3fa75eb34ac55f11f Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Mar 2026 18:09:37 +0500 Subject: [PATCH 30/75] Updated on 2026-08-14 --- .../v2/impl/amount/entity/SwapAmountUM.kt | 32 +++++- .../v2/impl/amount/model/SwapAmountModel.kt | 23 +++- .../impl/amount/model/SwapAmountQuoteUtils.kt | 86 +++++++++++---- .../converter/SwapAmountFieldConverter.kt | 1 - .../SwapAmountPrimaryReadyStateTransformer.kt | 1 + ...wapAmountSecondaryReadyStateTransformer.kt | 1 + .../SwapAmountSelectQuoteTransformer.kt | 103 +++++++++++------- .../SwapAmountSetQuotesTransformer.kt | 5 + .../impl/amount/ui/SwapAmountBlockContent.kt | 51 ++++----- .../ui/preview/SwapAmountContentPreview.kt | 6 +- .../confirm/model/SendWithSwapConfirmModel.kt | 9 +- 11 files changed, 215 insertions(+), 103 deletions(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index b5cdb3898e..c0f9222dd3 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -46,6 +46,7 @@ internal sealed class SwapAmountUM { // selected swap route val swapRateType: ExpressRateType, val swapRateMode: SwapRateMode, + val priceImpact: PriceImpact?, // swap models val swapCurrencies: SwapCurrencies, @@ -79,7 +80,6 @@ sealed class SwapAmountFieldUM { data class Content( override val amountType: SwapAmountType, override val amountField: AmountState, - val priceImpact: TextReference?, val title: TextReference, val subtitleLeft: TextReference, val subtitleRight: TextReference, @@ -90,9 +90,33 @@ sealed class SwapAmountFieldUM { } @Immutable -sealed class PriceImpactUM { +data class PriceImpact( + val value: TextReference, + val amountSignificance: AmountSignificance, + val type: Type, +) { - data object Empty : PriceImpactUM() + enum class Type { + NONE, LOW, MEDIUM, HIGH + } - data class Value(val value: Float) : PriceImpactUM() + enum class AmountSignificance { + LOW, MEDIUM, HIGH + } + + fun shouldDisableButton(): Boolean { + return type == Type.HIGH && amountSignificance == AmountSignificance.HIGH + } + + fun shouldShowWarning(): Boolean { + return type.ordinal > Type.LOW.ordinal || amountSignificance.ordinal > AmountSignificance.LOW.ordinal + } + + companion object { + val Empty = PriceImpact( + value = TextReference.EMPTY, + amountSignificance = AmountSignificance.LOW, + type = Type.NONE, + ) + } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 98c238edb1..90eea6d0fa 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -21,13 +21,14 @@ import com.tangem.datasource.local.swap.SwapBestRateAnimationStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.notifications.ShouldShowNotificationUseCase +import com.tangem.domain.quotes.GetCurrencyUSDQuoteUseCase import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions -import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.swap.models.* import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase @@ -58,13 +59,13 @@ import com.tangem.utils.coroutines.PeriodicTask import com.tangem.utils.coroutines.SingleTaskScheduler import com.tangem.utils.extensions.orZero import com.tangem.utils.isNullOrZero +import com.tangem.utils.logging.TangemLogger import com.tangem.utils.transformer.update import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.util.Locale import javax.inject.Inject @@ -93,6 +94,7 @@ internal class SwapAmountModel @Inject constructor( private val shouldShowNotificationUseCase: ShouldShowNotificationUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val getWalletsUseCase: GetWalletsUseCase, + private val getCurrencyUSDQuoteUseCase: GetCurrencyUSDQuoteUseCase, ) : Model(), SwapAmountClickIntents, SwapChooseProviderComponent.ModelCallback { private val params: SwapAmountComponentParams = paramsContainer.require() @@ -107,6 +109,9 @@ internal class SwapAmountModel @Inject constructor( private var secondaryMaximumAmountBoundary: EnterAmountBoundary? = null private var secondaryMinimumAmountBoundary: EnterAmountBoundary? = null + private var primaryFiatRateUSD: BigDecimal? = null + private var secondaryFiatRateUSD: BigDecimal? = null + private var userCountry: UserCountry = UserCountry.Other(Locale.getDefault().country) val bottomSheetNavigation: SlotNavigation = SlotNavigation() val rateInfoNavigation: SlotNavigation = SlotNavigation() @@ -177,6 +182,8 @@ internal class SwapAmountModel @Inject constructor( isBalanceHidden = params.isBalanceHidingFlow.value, primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, + primaryFiatRateUSD = primaryFiatRateUSD, + secondaryFiatRateUSD = secondaryFiatRateUSD, ), ) } @@ -218,7 +225,7 @@ internal class SwapAmountModel @Inject constructor( val selectedProvider = amountUM.selectedQuote.provider ?: return swapAmountAlertFactory.priceImpactAlert( - hasPriceImpact = (amountUM.secondaryAmount as? SwapAmountFieldUM.Content)?.priceImpact != null, + hasPriceImpact = amountUM.priceImpact != null, currencySymbol = amountUM.primaryCryptoCurrencyStatus.currency.symbol, provider = selectedProvider, ) @@ -560,6 +567,8 @@ internal class SwapAmountModel @Inject constructor( val secondaryStatus = secondaryCurrency?.currencyStatus val primaryStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus if (secondaryStatus != null && primaryStatus != null) { + val rawId = secondaryStatus.currency.id.rawCurrencyId + secondaryFiatRateUSD = rawId?.let { id -> getCurrencyUSDQuoteUseCase(id) } initCurrencies(primaryStatus, secondaryStatus) val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1 uiState.transformerUpdate( @@ -611,7 +620,13 @@ internal class SwapAmountModel @Inject constructor( ) primaryMaximumAmountBoundary = MaxEnterAmountConverter().convert(primaryStatus) + val primaryRawId = primaryStatus.currency.id.rawCurrencyId + primaryFiatRateUSD = primaryRawId?.let { id -> getCurrencyUSDQuoteUseCase(id) } + if (secondaryStatus != null) { + val secondaryRawId = secondaryStatus.currency.id.rawCurrencyId + secondaryFiatRateUSD = secondaryRawId?.let { id -> getCurrencyUSDQuoteUseCase(id) } + secondaryMinimumAmountBoundary = EnterAmountBoundary( amount = getMinimumTransactionAmountSyncUseCase .invoke( @@ -727,6 +742,8 @@ internal class SwapAmountModel @Inject constructor( isBalanceHidden = params.isBalanceHidingFlow.value, primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, + primaryFiatRateUSD = primaryFiatRateUSD, + secondaryFiatRateUSD = secondaryFiatRateUSD, ), ) feeSelectorReloadTrigger.triggerUpdate() diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt index f0fee9face..3301fe3f9d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt @@ -1,16 +1,17 @@ package com.tangem.features.swap.v2.impl.amount.model -import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapDirection +import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM -import com.tangem.utils.extensions.isZero -import com.tangem.utils.isNullOrZero +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.utils.StringsSigns +import com.tangem.utils.extensions.orZero import java.math.BigDecimal import java.math.RoundingMode import kotlin.math.min @@ -20,31 +21,74 @@ internal object SwapAmountQuoteUtils { private const val MAX_DECIMALS = 8 private const val MIN_DECIMALS = 2 - fun calculatePriceImpact( - fromTokenAmount: BigDecimal, - toTokenAmount: BigDecimal, + private val PRICE_IMPACT_AMOUNT_MIN_THRESHOLD = 25.toBigDecimal() // in USD + private val PRICE_IMPACT_AMOUNT_MAX_THRESHOLD = 5000.toBigDecimal() // in USD + private val PRICE_IMPACT_LOW_THRESHOLD = 0.1.toBigDecimal() // 10% + private val PRICE_IMPACT_HIGH_THRESHOLD = 0.5.toBigDecimal() // 50% + + @Suppress("ComplexMethod", "LongParameterList") + internal fun calculatePriceImpact( + quoteContent: SwapQuoteUM.Content?, swapDirection: SwapDirection, + primaryFiatRateUSD: BigDecimal?, + secondaryFiatRateUSD: BigDecimal?, primaryCryptoCurrencyStatus: CryptoCurrencyStatus, - secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, - ): TextReference? { + secondaryCryptoCurrencyStatus: CryptoCurrencyStatus?, + ): PriceImpact { + if (quoteContent == null) return PriceImpact.Empty + + val fromAmount = quoteContent.fromAmount ?: return PriceImpact.Empty + val toAmount = quoteContent.toAmount + val (fromRate, toRate) = if (swapDirection == SwapDirection.Direct) { - primaryCryptoCurrencyStatus.value.fiatRate to secondaryCryptoCurrencyStatus.value.fiatRate + primaryCryptoCurrencyStatus.value.fiatRate to secondaryCryptoCurrencyStatus?.value?.fiatRate } else { - secondaryCryptoCurrencyStatus.value.fiatRate to primaryCryptoCurrencyStatus.value.fiatRate + secondaryCryptoCurrencyStatus?.value?.fiatRate to primaryCryptoCurrencyStatus.value.fiatRate } - val isRatesNull = fromRate.isNullOrZero() || toRate.isNullOrZero() - val isAmountNull = fromTokenAmount.isZero() || toTokenAmount.isZero() - if (isRatesNull || isAmountNull) return null - - val fromTokenFiatValue = fromTokenAmount.multiply(fromRate) - val toTokenFiatValue = toTokenAmount.multiply(toRate) - - val value = BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP) - - return stringReference("$(-${value.format { percent(withoutSign = false) }})").takeIf { - value > 0.1.toBigDecimal() + val fromRateUsd = if (swapDirection == SwapDirection.Direct) { + primaryFiatRateUSD + } else { + secondaryFiatRateUSD } + + val fromTokenFiatValue = fromRate?.let { fromAmount.multiply(fromRate).orZero() } + val toTokenFiatValue = toRate?.let { toAmount.multiply(toRate) } + + val isFromNotZero = fromTokenFiatValue != null && fromTokenFiatValue != BigDecimal.ZERO + val isToNotZero = toTokenFiatValue != null && toTokenFiatValue != BigDecimal.ZERO + + val value = if (isFromNotZero && isToNotZero) { + BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP) + } else { + null + } + + val fromAmountUSD = fromAmount.multiply(fromRateUsd.orZero()) + + val type = when { + value == null -> PriceImpact.Type.NONE + value < PRICE_IMPACT_LOW_THRESHOLD -> PriceImpact.Type.LOW + value in PRICE_IMPACT_LOW_THRESHOLD..PRICE_IMPACT_HIGH_THRESHOLD -> PriceImpact.Type.MEDIUM + else -> PriceImpact.Type.HIGH + } + + val amountSignificance = when { + fromAmountUSD <= PRICE_IMPACT_AMOUNT_MIN_THRESHOLD -> PriceImpact.AmountSignificance.LOW + fromAmountUSD > PRICE_IMPACT_AMOUNT_MAX_THRESHOLD -> PriceImpact.AmountSignificance.HIGH + else -> PriceImpact.AmountSignificance.MEDIUM + } + + return PriceImpact( + value = stringReference("(${StringsSigns.MINUS}${value.format { percent() }})"), + amountSignificance = amountSignificance, + type = type, + ) + } + + fun isHighPriceImpact(amountUM: SwapAmountUM): Boolean { + val priceImpact = (amountUM as? SwapAmountUM.Content)?.priceImpact ?: return false + return priceImpact.shouldDisableButton() } fun calculateRate(fromAmount: BigDecimal, toAmount: BigDecimal, toAmountDecimals: Int): BigDecimal { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt index 6bfb4de5bd..810a528c2d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt @@ -58,7 +58,6 @@ internal class SwapAmountFieldConverter( subtitleEllipsisLeft = subtitles.subtitleEllipsisLeft, subtitleRight = subtitles.subtitleRight, subtitleEllipsisRight = subtitles.subtitleEllipsisRight, - priceImpact = null, isClickEnabled = true, amountField = AmountStateConverter( clickIntents = clickIntents, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt index e2d648a6c9..51d33f01b5 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt @@ -64,6 +64,7 @@ internal class SwapAmountPrimaryReadyStateTransformer( appCurrency = appCurrency, isShowBestRateAnimation = isShowBestRateAnimation, isShowFCAWarning = false, + priceImpact = null, swapRateMode = (prevState as? SwapAmountUM.Content)?.swapRateMode ?: SwapRateMode.FLOAT_ONLY, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt index cc49a10c46..2fb41d9e71 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt @@ -80,6 +80,7 @@ internal class SwapAmountSecondaryReadyStateTransformer( appCurrency = appCurrency, isShowBestRateAnimation = isShowBestRateAnimation, isShowFCAWarning = false, + priceImpact = null, swapRateMode = swapRateMode, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt index 627c8e5881..8caee7dee2 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt @@ -7,17 +7,18 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.swap.models.SwapAmountType +import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.calculatePriceImpact import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountErrorConverter import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountUpdateSubtitleConverter -import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA -import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal +@Suppress("LongParameterList") internal class SwapAmountSelectQuoteTransformer( private val quoteUM: SwapQuoteUM, private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, @@ -26,35 +27,64 @@ internal class SwapAmountSelectQuoteTransformer( private val isBalanceHidden: Boolean, private val primaryMaximumAmountBoundary: EnterAmountBoundary? = null, private val primaryMinimumAmountBoundary: EnterAmountBoundary? = null, + private val primaryFiatRateUSD: BigDecimal?, + private val secondaryFiatRateUSD: BigDecimal?, ) : Transformer { - @Suppress("CyclomaticComplexMethod", "LongMethod") override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState - val isPrimarySelected = prevState.selectedAmountType == SwapAmountType.From - val isSecondarySelected = prevState.selectedAmountType == SwapAmountType.To - - val primaryProviderErrorConverter = SwapAmountErrorConverter( - cryptoCurrency = prevState.primaryCryptoCurrencyStatus.currency, - ) - val secondaryProviderErrorConverter = prevState.secondaryCryptoCurrencyStatus?.let { - SwapAmountErrorConverter(cryptoCurrency = it.currency) - } - val quoteContent = quoteUM as? SwapQuoteUM.Content - val fromAmount = quoteContent?.fromAmount - val toAmount = quoteContent?.toAmount - - val primarySwapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content - val secondarySwapAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content val subtitleConverter = SwapAmountUpdateSubtitleConverter( selectedAmountType = prevState.selectedAmountType, isBalanceHidden = isBalanceHidden, ) - val newPrimaryAmount = when { + val newPrimaryAmount = getPrimaryAmount( + prevState = prevState, + quoteContent = quoteContent, + subtitleConverter = subtitleConverter, + ) + + val newSecondaryAmount = getSecondaryAmount( + prevState = prevState, + quoteContent = quoteContent, + subtitleConverter = subtitleConverter, + ) + + val priceImpact = calculatePriceImpact( + quoteContent = quoteContent, + swapDirection = prevState.swapDirection, + primaryFiatRateUSD = primaryFiatRateUSD, + secondaryFiatRateUSD = secondaryFiatRateUSD, + primaryCryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, + ) + + return prevState.copy( + isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, + selectedQuote = quoteUM, + isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, + primaryAmount = newPrimaryAmount, + priceImpact = priceImpact, + secondaryAmount = newSecondaryAmount, + ) + } + + private fun getPrimaryAmount( + prevState: SwapAmountUM.Content, + quoteContent: SwapQuoteUM.Content?, + subtitleConverter: SwapAmountUpdateSubtitleConverter, + ): SwapAmountFieldUM { + val isPrimarySelected = prevState.selectedAmountType == SwapAmountType.From + val primarySwapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content + + val primaryProviderErrorConverter = SwapAmountErrorConverter( + cryptoCurrency = prevState.primaryCryptoCurrencyStatus.currency, + ) + val fromAmount = quoteContent?.fromAmount + return when { fromAmount != null && primaryMaximumAmountBoundary != null -> { primarySwapAmountField?.let { fromField -> subtitleConverter.updateSubtitles( @@ -103,22 +133,26 @@ internal class SwapAmountSelectQuoteTransformer( } else -> prevState.primaryAmount } + } - val newSecondaryAmount = if ( + @Suppress("CyclomaticComplexMethod") + private fun getSecondaryAmount( + prevState: SwapAmountUM.Content, + quoteContent: SwapQuoteUM.Content?, + subtitleConverter: SwapAmountUpdateSubtitleConverter, + ): SwapAmountFieldUM { + val isSecondarySelected = prevState.selectedAmountType == SwapAmountType.To + val secondaryProviderErrorConverter = prevState.secondaryCryptoCurrencyStatus?.let { + SwapAmountErrorConverter(cryptoCurrency = it.currency) + } + val fromAmount = quoteContent?.fromAmount + val toAmount = quoteContent?.toAmount + val secondarySwapAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content + return if ( prevState.secondaryCryptoCurrencyStatus != null && secondaryMaximumAmountBoundary != null && secondarySwapAmountField != null ) { - val fromAmountForPriceImpact = fromAmount - ?: (prevState.primaryAmount.amountField as? AmountState.Data) - ?.amountTextField?.cryptoAmount?.value.orZero() - val priceImpact = calculatePriceImpact( - swapDirection = prevState.swapDirection, - fromTokenAmount = fromAmountForPriceImpact, - toTokenAmount = toAmount.orZero(), - primaryCryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, - secondaryCryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, - ) val isAmountEmpty = toAmount == null val secondaryAmountError = if (isSecondarySelected) { (quoteUM as? SwapQuoteUM.Error)?.expressError @@ -166,19 +200,10 @@ internal class SwapAmountSelectQuoteTransformer( isAmountEmpty = isAmountEmpty, displayAmount = toAmount, ).copy( - priceImpact = priceImpact, amountField = secondaryAmountFieldWithError, ) } else { prevState.secondaryAmount } - - return prevState.copy( - isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, - selectedQuote = quoteUM, - isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, - primaryAmount = newPrimaryAmount, - secondaryAmount = newSecondaryAmount, - ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index 9b84a536bf..fb4abed9ee 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -18,6 +18,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal +@Suppress("LongParameterList") internal class SwapAmountSetQuotesTransformer( private val quotes: List, private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, @@ -27,6 +28,8 @@ internal class SwapAmountSetQuotesTransformer( private val isBalanceHidden: Boolean, private val primaryMaximumAmountBoundary: EnterAmountBoundary? = null, private val primaryMinimumAmountBoundary: EnterAmountBoundary? = null, + private val primaryFiatRateUSD: BigDecimal?, + private val secondaryFiatRateUSD: BigDecimal?, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { @@ -53,6 +56,8 @@ internal class SwapAmountSetQuotesTransformer( val updatedState = SwapAmountSelectQuoteTransformer( quoteUM = selectedQuote, + primaryFiatRateUSD = primaryFiatRateUSD, + secondaryFiatRateUSD = secondaryFiatRateUSD, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, isNeedApplyFCARestrictions = isNeedApplyFcaRestrictions && diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index d75eb733a2..8ffffab5a9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -37,9 +37,9 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.swap.v2.impl.R -import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.domain.swap.models.SwapAmountType +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent @@ -86,7 +86,7 @@ internal fun SwapAmountBlockContent( val quoteContent = amountUM.selectedQuote as? SwapQuoteUM.Content val isBestRate = quoteContent?.diffPercent is SwapQuoteUM.Content.DifferencePercent.Best SwapChooseProviderContent( - isBestRate = isBestRate, + isBestRate = isBestRate && amountUM.priceImpact?.shouldShowWarning() == true, isSingleProvider = quoteContent?.isSingleProvider == true, showBestRateAnimation = amountUM.isShowBestRateAnimation, expressProvider = amountUM.selectedQuote.provider, @@ -119,13 +119,6 @@ private fun ConstraintLayoutScope.SwapAmountBlock( start.linkTo(parent.start) end.linkTo(parent.end) }, - extraContent = { - SwapPriceImpact( - amountFieldUM = amountUM.primaryAmount, - selectedAmountType = amountUM.selectedAmountType, - onInfoClick = onInfoClick, - ) - }, ) AmountBlockV2( amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy( @@ -140,35 +133,33 @@ private fun ConstraintLayoutScope.SwapAmountBlock( end.linkTo(parent.end) }, extraContent = { - SwapPriceImpact( - amountFieldUM = amountUM.secondaryAmount, - selectedAmountType = amountUM.selectedAmountType, - onInfoClick = onInfoClick, - ) + if (amountUM.secondaryAmount.amountType == SwapAmountType.To) { + SwapPriceImpact( + priceImpact = amountUM.priceImpact, + onInfoClick = onInfoClick, + ) + } }, ) } @Composable -private fun SwapPriceImpact( - amountFieldUM: SwapAmountFieldUM, - selectedAmountType: SwapAmountType, - onInfoClick: () -> Unit, -) { - if (amountFieldUM.amountType == selectedAmountType) return - - val priceImpact = (amountFieldUM as? SwapAmountFieldUM.Content)?.priceImpact - val iconColor = if (priceImpact != null) { - TangemTheme.colors.icon.attention - } else { - TangemTheme.colors.icon.informative +private fun SwapPriceImpact(priceImpact: PriceImpact?, onInfoClick: () -> Unit) { + val iconColor = when (priceImpact?.type) { + PriceImpact.Type.MEDIUM -> TangemTheme.colors.icon.attention + PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning + else -> TangemTheme.colors.icon.informative } - if (priceImpact != null) { + if (priceImpact != null && priceImpact.type.ordinal > PriceImpact.Type.LOW.ordinal) { Text( - text = priceImpact.resolveReference(), + text = priceImpact.value.resolveReference(), style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.attention, + color = when (priceImpact.type) { + PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning + PriceImpact.Type.MEDIUM -> TangemTheme.colors.text.attention + else -> TangemTheme.colors.text.tertiary + }, ) } Icon( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index 6f354c847a..f15aca16d8 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -12,11 +12,11 @@ import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network +import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.swap.models.SwapRateMode import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM -import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.utils.StringsSigns @@ -95,6 +95,7 @@ internal data object SwapAmountContentPreview { appCurrency = AppCurrency.Default, isShowBestRateAnimation = false, isShowFCAWarning = false, + priceImpact = null, swapRateMode = SwapRateMode.FLOAT_ONLY, ) @@ -105,7 +106,6 @@ internal data object SwapAmountContentPreview { title = stringReference("Tether"), subtitleLeft = stringReference("11 101,123123456 BTC"), subtitleRight = stringReference(" ${StringsSigns.DOT} 1 212,12 $"), - priceImpact = null, isClickEnabled = false, subtitleEllipsisLeft = TextEllipsis.OffsetEnd(3), subtitleEllipsisRight = TextEllipsis.OffsetEnd(1), @@ -116,7 +116,6 @@ internal data object SwapAmountContentPreview { accountTitleUM = AccountTitleUM.Text(stringReference("Amount to receive")), ), title = stringReference("Shiba Inu"), - priceImpact = stringReference("(-10%)"), subtitleLeft = TextReference.EMPTY, subtitleRight = TextReference.EMPTY, isClickEnabled = false, @@ -135,6 +134,7 @@ internal data object SwapAmountContentPreview { isPrimaryButtonEnabled = true, isShowBestRateAnimation = false, isShowFCAWarning = true, + priceImpact = null, swapRateMode = SwapRateMode.FLOAT_AND_FIXED, ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 3c685ed2c6..34b87c65bb 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -52,6 +52,7 @@ import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.SwapAmountReduceTrigger import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils import com.tangem.features.swap.v2.impl.common.ConfirmData import com.tangem.features.swap.v2.impl.common.SwapAlertFactory import com.tangem.features.swap.v2.impl.common.SwapUtils.INCREASE_GAS_LIMIT_FOR_CEX @@ -126,7 +127,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( private val feeUMV2 get() = uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Content - val secondaryCurrencyStatus: CryptoCurrencyStatus? = amountUM?.secondaryCryptoCurrencyStatus + private val secondaryCurrencyStatus: CryptoCurrencyStatus? = amountUM?.secondaryCryptoCurrencyStatus val secondaryCurrency: CryptoCurrency = requireNotNull(amountUM?.secondaryCryptoCurrencyStatus?.currency) { "Crypto currency must not be null" } @@ -166,6 +167,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( quote = amountUM?.selectedQuote, rateType = amountUM?.swapRateType, amountType = amountUM?.selectedAmountType ?: SwapAmountType.From, + priceImpact = amountUM?.priceImpact, ) } @@ -444,6 +446,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( userWalletId = params.userWallet.walletId, enteredFromAmount = confirmData.enteredFromAmount, fromCryptoCurrencyStatus = confirmData.fromCryptoCurrencyStatus, + priceImpact = confirmData.priceImpact, ), ) uiState.transformerUpdate( @@ -461,9 +464,11 @@ internal class SendWithSwapConfirmModel @Inject constructor( uiState.update { state -> val feeUM = state.feeSelectorUM as? FeeSelectorUM.Content val isTransactionInProcess = (state.confirmUM as? ConfirmUM.Content)?.isTransactionInProcess == true + val isHighPriceImpact = SwapAmountQuoteUtils.isHighPriceImpact(state.amountUM) + val isPrimaryButtonEnabled = !hasError && feeUM != null && !isTransactionInProcess && !isHighPriceImpact state.copy( confirmUM = (state.confirmUM as? ConfirmUM.Content)?.copy( - isPrimaryButtonEnabled = !hasError && feeUM != null && !isTransactionInProcess, + isPrimaryButtonEnabled = isPrimaryButtonEnabled, ) ?: state.confirmUM, ) } From 70617df9ad424e34b4798b1f19413d96acb4b6ea Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Mar 2026 18:09:56 +0500 Subject: [PATCH 31/75] Updated on 2026-08-14 --- .../swap/v2/impl/common/ConfirmData.kt | 2 ++ .../SwapNotificationsComponent.kt | 2 ++ .../entity/SwapNotificationUM.kt | 11 ++++++++ .../model/SwapNotificationsModel.kt | 25 +++++++++++++++++-- .../ui/SwapNotificationsContent.kt | 1 + .../confirm/SendWithSwapConfirmComponent.kt | 1 + 6 files changed, 40 insertions(+), 2 deletions(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt index f42349e6c2..98d185084b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt @@ -6,6 +6,7 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import java.math.BigDecimal @@ -24,4 +25,5 @@ internal data class ConfirmData( val quote: SwapQuoteUM?, val rateType: ExpressRateType?, val amountType: SwapAmountType, + val priceImpact: PriceImpact?, ) \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt index db6c8d8dbe..ecd9813de0 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt @@ -9,6 +9,7 @@ import com.tangem.domain.express.models.ExpressError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact import java.math.BigDecimal import com.tangem.features.swap.v2.impl.notifications.model.SwapNotificationsModel import com.tangem.features.swap.v2.impl.notifications.ui.swapNotifications @@ -49,6 +50,7 @@ internal class SwapNotificationsComponent( val userWalletId: UserWalletId? = null, val enteredFromAmount: BigDecimal? = null, val fromCryptoCurrencyStatus: CryptoCurrencyStatus? = null, + val priceImpact: PriceImpact? = null, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt index fc14191b9b..de64062b75 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt @@ -82,5 +82,16 @@ internal object SwapNotificationUM { onClick = onConfirmClick, ), ) + + data object HighPriceImpact : Warning( + title = resourceReference(R.string.swapping_high_price_impact_title), + subtitle = resourceReference(R.string.swapping_high_price_impact_text), + ) + + data object TradeTooHigh : Warning( + title = resourceReference(R.string.swapping_trade_too_large_title), + subtitle = resourceReference(R.string.swapping_trade_too_large_text), + iconResId = R.drawable.ic_alert_circle_24, + ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt index 73dc591264..7a265478bb 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt @@ -11,13 +11,13 @@ import com.tangem.domain.express.models.ExpressError import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase import com.tangem.domain.utils.convertToSdkAmount import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger +import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact import com.tangem.features.swap.v2.impl.notifications.DefaultSwapNotificationsUpdateTrigger import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent.Params.SwapNotificationData import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateListener import com.tangem.features.swap.v2.impl.notifications.entity.SwapNotificationUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import java.math.BigDecimal import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import java.math.BigDecimal import javax.inject.Inject @ModelScoped @@ -68,9 +69,16 @@ internal class SwapNotificationsModel @Inject constructor( addInsufficientFundsNotification() addExpressErrorNotification() addDestinationTagRequiredNotification() + maybeAddPriceImpactNotification() } - swapNotificationsUpdateTrigger.callbackHasError(notifications.isNotEmpty()) + val hasErrorNotification = notifications + .filterNot { notification -> + notification == SwapNotificationUM.Warning.TradeTooHigh || + notification == SwapNotificationUM.Warning.HighPriceImpact + } + .isNotEmpty() + swapNotificationsUpdateTrigger.callbackHasError(hasErrorNotification) uiState.value = notifications.toImmutableList() } @@ -135,4 +143,17 @@ internal class SwapNotificationsModel @Inject constructor( add(errorNotification) } + + private fun MutableList.maybeAddPriceImpactNotification() { + val priceImpact = notificationData.priceImpact ?: return + if (!priceImpact.shouldShowWarning()) return + + val notification = when (priceImpact.type) { + PriceImpact.Type.HIGH -> SwapNotificationUM.Warning.TradeTooHigh + PriceImpact.Type.MEDIUM -> SwapNotificationUM.Warning.HighPriceImpact + else -> return + } + + add(notification) + } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/ui/SwapNotificationsContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/ui/SwapNotificationsContent.kt index 9f1ec2d283..ae06a1fc78 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/ui/SwapNotificationsContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/ui/SwapNotificationsContent.kt @@ -38,6 +38,7 @@ internal fun LazyListScope.swapNotifications( else -> TangemTheme.colors.button.disabled }, iconTint = when (item) { + is SwapNotificationUM.Warning.TradeTooHigh -> TangemTheme.colors.icon.warning is SwapNotificationUM.Error, is NotificationUM.Error.TokenExceedsBalance, is NotificationUM.Warning, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index 98b57000b7..1b837de7c9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -139,6 +139,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( userWalletId = params.userWallet.walletId, enteredFromAmount = model.confirmData.enteredFromAmount, fromCryptoCurrencyStatus = model.confirmData.fromCryptoCurrencyStatus, + priceImpact = model.confirmData.priceImpact, ), ), ) From dfbec1dee4ab741b5bec45a902c1e68c64322e68 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Mar 2026 18:10:09 +0500 Subject: [PATCH 32/75] Updated on 2026-08-14 --- .../impl/amount/model/SwapAmountQuoteUtils.kt | 6 +-- .../impl/amount/ui/SwapAmountBlockContent.kt | 2 +- .../feature/swap/domain/SwapInteractorImpl.kt | 8 ++-- .../swap/domain/models/ui/SwapState.kt | 4 ++ .../tangem/feature/swap/ui/StateBuilder.kt | 44 +++++++++---------- 5 files changed, 34 insertions(+), 30 deletions(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt index 3301fe3f9d..0c883b505c 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt @@ -34,10 +34,10 @@ internal object SwapAmountQuoteUtils { secondaryFiatRateUSD: BigDecimal?, primaryCryptoCurrencyStatus: CryptoCurrencyStatus, secondaryCryptoCurrencyStatus: CryptoCurrencyStatus?, - ): PriceImpact { - if (quoteContent == null) return PriceImpact.Empty + ): PriceImpact? { + if (quoteContent == null) return null - val fromAmount = quoteContent.fromAmount ?: return PriceImpact.Empty + val fromAmount = quoteContent.fromAmount ?: return null val toAmount = quoteContent.toAmount val (fromRate, toRate) = if (swapDirection == SwapDirection.Direct) { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index 8ffffab5a9..6db639d48a 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -86,7 +86,7 @@ internal fun SwapAmountBlockContent( val quoteContent = amountUM.selectedQuote as? SwapQuoteUM.Content val isBestRate = quoteContent?.diffPercent is SwapQuoteUM.Content.DifferencePercent.Best SwapChooseProviderContent( - isBestRate = isBestRate && amountUM.priceImpact?.shouldShowWarning() == true, + isBestRate = isBestRate && amountUM.priceImpact?.shouldShowWarning() != true, isSingleProvider = quoteContent?.isSingleProvider == true, showBestRateAnimation = amountUM.isShowBestRateAnimation, expressProvider = amountUM.selectedQuote.provider, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 2a22cca812..405c0aa43b 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -68,7 +68,6 @@ import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.* import com.tangem.lib.crypto.BlockchainUtils.SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES import com.tangem.utils.coroutines.runSuspendCatching -import com.tangem.utils.extensions.orZero import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -2457,7 +2456,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( if (fromQuoteStatus == null) return PriceImpact.Empty val fromTokenFiatValue = fromTokenAmount.multiply(fromQuoteStatus.fiatRate) - val toTokenFiatValue = toTokenAmount.multiply(toRate.orZero()) + val toTokenFiatValue = toRate?.let { toTokenAmount.multiply(toRate) } ?: return PriceImpact.Empty + val value = BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP) val fromAmountUSD = if (fromQuoteStatus.fiatRateUSD != BigDecimal.ZERO) { @@ -2467,7 +2467,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( } val amountSignificance = when { - fromAmountUSD < PRICE_IMPACT_AMOUNT_MIN_THRESHOLD -> PriceImpact.AmountSignificance.LOW + fromAmountUSD <= PRICE_IMPACT_AMOUNT_MIN_THRESHOLD -> PriceImpact.AmountSignificance.LOW fromAmountUSD > PRICE_IMPACT_AMOUNT_MAX_THRESHOLD -> PriceImpact.AmountSignificance.HIGH else -> PriceImpact.AmountSignificance.MEDIUM } @@ -2552,7 +2552,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( companion object { private const val INCREASE_GAS_LIMIT_FOR_DEX = 112 // 12% private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5% - private val PRICE_IMPACT_AMOUNT_MIN_THRESHOLD = 50.toBigDecimal() // in USD + private val PRICE_IMPACT_AMOUNT_MIN_THRESHOLD = 25.toBigDecimal() // in USD private val PRICE_IMPACT_AMOUNT_MAX_THRESHOLD = 5000.toBigDecimal() // in USD private val PRICE_IMPACT_LOW_THRESHOLD = 0.1.toBigDecimal() // 10% private val PRICE_IMPACT_HIGH_THRESHOLD = 0.5.toBigDecimal() // 50% diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index fa1c9bdf73..a630100e2e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -66,6 +66,10 @@ data class PriceImpact( return type == Type.HIGH && amountSignificance == AmountSignificance.HIGH } + fun shouldShowWarning(): Boolean { + return type.ordinal > Type.LOW.ordinal || amountSignificance.ordinal > AmountSignificance.LOW.ordinal + } + companion object { val Empty = PriceImpact( value = BigDecimal.ZERO, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 6ba8b6a6e4..b6613eba32 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -387,26 +387,26 @@ internal class StateBuilder( .formatToUIRepresentation() .appendApproximateSign(), ), - amountEquivalent = combinedReference( - getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat), - stringReference(StringsSigns.WHITE_SPACE), - styledStringReference( - priceImpact.value.format { - percent(withoutSign = false) - }, - spanStyleReference = { - SpanStyle( - color = when (priceImpact.type) { - PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning - PriceImpact.Type.MEDIUM -> TangemTheme.colors.text.attention - PriceImpact.Type.LOW, - PriceImpact.Type.NONE, - -> TangemTheme.colors.text.tertiary - }, - ) - }, - ), - ), + amountEquivalent = if (priceImpact.type.ordinal > PriceImpact.Type.LOW.ordinal) { + combinedReference( + getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat), + stringReference(StringsSigns.WHITE_SPACE), + styledStringReference( + value = "(${StringsSigns.MINUS}${priceImpact.value.format { percent() }})", + spanStyleReference = { + SpanStyle( + color = when (priceImpact.type) { + PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning + PriceImpact.Type.MEDIUM -> TangemTheme.colors.text.attention + else -> TangemTheme.colors.text.tertiary + }, + ) + }, + ), + ) + } else { + getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat) + }, token = toCurrencyStatus, tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, coinId = toCurrencyStatus.currency.network.backendId, @@ -436,7 +436,7 @@ internal class StateBuilder( ), changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), providerState = swapProvider.convertToContentClickableProviderState( - isBestRate = bestRatedProviderId == swapProvider.providerId, + isBestRate = bestRatedProviderId == swapProvider.providerId && !priceImpact.shouldShowWarning(), fromTokenInfo = quoteModel.fromTokenInfo, toTokenInfo = quoteModel.toTokenInfo, isNeedBestRateBadge = isNeedBestRateBadge, @@ -444,7 +444,7 @@ internal class StateBuilder( onProviderClick = actions.onProviderClick, needApplyFCARestrictions = needApplyFCARestrictions, ), - priceImpact = quoteModel.priceImpact, + priceImpact = priceImpact, tosState = createTosState(swapProvider), shouldShowMaxAmount = shouldShowMaxAmount(fromToken, toCurrencyStatus.currency), ) From 203b1c52cc1153d00fcd23c8f19584ae430fc125 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Mar 2026 14:35:20 +0700 Subject: [PATCH 33/75] Updated on 2026-08-14 --- .../data/swap/DefaultSwapRepositoryV2.kt | 2 - data/tokens/build.gradle.kts | 1 + .../tangem/data/tokens/di/TokensDataModule.kt | 3 ++ .../DefaultCurrencyChecksRepository.kt | 6 +++ .../transaction/DefaultMemoValidatorFacade.kt | 42 +++++++++++++++++++ .../DefaultWalletAddressServiceRepository.kt | 30 +++---------- .../transaction/di/TransactionDataModule.kt | 17 ++++++++ .../model/warnings/CryptoCurrencyCheck.kt | 1 + .../domain/tokens/GetCurrencyCheckUseCase.kt | 9 ++++ .../repository/CurrencyChecksRepository.kt | 3 ++ .../domain/transaction/MemoValidatorFacade.kt | 21 ++++++++++ .../WalletAddressServiceRepository.kt | 4 +- .../usecase/ValidateWalletMemoUseCase.kt | 1 - .../notifications/model/NotificationsModel.kt | 21 ++++++++++ gradle/tangem_dependencies.toml | 2 +- .../blockchainsdk/BlockchainSDKFactory.kt | 4 ++ .../DefaultBlockchainSDKFactory.kt | 14 +++++++ 17 files changed, 152 insertions(+), 29 deletions(-) create mode 100644 data/transaction/src/main/java/com/tangem/data/transaction/DefaultMemoValidatorFacade.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/MemoValidatorFacade.kt diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index d51b43b1e1..a2c353ef57 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -478,8 +478,6 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( } private val MEMO_RESTRICTED_NETWORKS = setOf( - Blockchain.XRP.toNetworkId(), - Blockchain.Stellar.toNetworkId(), Blockchain.InternetComputer.toNetworkId(), Blockchain.Casper.toNetworkId(), Blockchain.Algorand.toNetworkId(), diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index f25b6ec88e..de7a7ad1ff 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory.models) implementation(projects.domain.walletManager) + implementation(projects.domain.transaction) implementation(projects.domain.wallets.models) // endregion 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 9165cfa1b2..dc5802379d 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 @@ -13,6 +13,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository +import com.tangem.domain.transaction.MemoValidatorFacade import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -47,10 +48,12 @@ internal object TokensDataModule { @Singleton fun provideCurrencyChecksRepository( walletManagersFacade: WalletManagersFacade, + memoValidatorFacade: MemoValidatorFacade, coroutineDispatcherProvider: CoroutineDispatcherProvider, ): CurrencyChecksRepository { return DefaultCurrencyChecksRepository( walletManagersFacade = walletManagersFacade, + memoValidatorFacade = memoValidatorFacade, coroutineDispatchers = coroutineDispatcherProvider, ) } 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 b7d34b0d39..9f1b39e4d4 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 @@ -14,6 +14,7 @@ import com.tangem.domain.tokens.model.CurrencyAmount import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.repository.CurrencyChecksRepository +import com.tangem.domain.transaction.MemoValidatorFacade import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.isZero @@ -23,6 +24,7 @@ import java.math.BigDecimal internal class DefaultCurrencyChecksRepository( private val walletManagersFacade: WalletManagersFacade, + private val memoValidatorFacade: MemoValidatorFacade, private val coroutineDispatchers: CoroutineDispatcherProvider, ) : CurrencyChecksRepository { @@ -112,6 +114,10 @@ internal class DefaultCurrencyChecksRepository( return if (manager is ReserveAmountProvider) manager.isAccountFunded(address) else true } + override suspend fun checkIfMemoRequired(network: Network, address: String): Boolean { + return memoValidatorFacade.isMemoRequired(network, address) + } + override suspend fun checkUtxoAmountLimit( userWalletId: UserWalletId, network: Network, diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultMemoValidatorFacade.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultMemoValidatorFacade.kt new file mode 100644 index 0000000000..adb5b2be26 --- /dev/null +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultMemoValidatorFacade.kt @@ -0,0 +1,42 @@ +package com.tangem.data.transaction + +import com.tangem.blockchain.common.memo.MemoState +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.blockchainsdk.utils.toBlockchain +import com.tangem.domain.models.network.Network +import com.tangem.domain.transaction.MemoValidatorFacade +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext + +internal class DefaultMemoValidatorFacade( + private val blockchainSDKFactory: BlockchainSDKFactory, + private val dispatchers: CoroutineDispatcherProvider, +) : MemoValidatorFacade { + + override suspend fun isMemoRequired(network: Network, destinationAddress: String): Boolean = + withContext(dispatchers.io) { + val blockchain = network.toBlockchain() + val factory = blockchainSDKFactory.getMemoValidatorFactorySync() ?: return@withContext false + val validator = factory.create(blockchain) + when (val result = validator.isMemoRequired(destinationAddress)) { + is Result.Success -> result.data + is Result.Failure -> false + } + } + + override suspend fun validateMemo(network: Network, memo: String): Boolean = withContext(dispatchers.io) { + val blockchain = network.toBlockchain() + val factory = blockchainSDKFactory.getMemoValidatorFactorySync() ?: return@withContext true + val validator = factory.create(blockchain) + when (val result = validator.validateMemo(memo)) { + is Result.Success -> when (result.data) { + MemoState.Valid, + MemoState.NotSupported, + -> true + MemoState.Invalid -> false + } + is Result.Failure -> true + } + } +} \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt index 5c2bbe6654..d1fa3a1301 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt @@ -6,12 +6,10 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.NameResolver import com.tangem.blockchain.common.ResolveAddressResult import com.tangem.blockchain.common.ReverseResolveAddressResult -import com.tangem.blockchain.common.TransactionValidator -import com.tangem.blockchain.common.memo.MemoState -import com.tangem.blockchain.extensions.Result import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.MemoValidatorFacade import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.ParsedQrCode @@ -21,6 +19,7 @@ import kotlinx.coroutines.withContext class DefaultWalletAddressServiceRepository( private val walletManagersFacade: WalletManagersFacade, + private val memoValidatorFacade: MemoValidatorFacade, private val dispatchers: CoroutineDispatcherProvider, ) : WalletAddressServiceRepository { @@ -96,28 +95,11 @@ class DefaultWalletAddressServiceRepository( } } - override suspend fun validateMemo(userWalletId: UserWalletId, network: Network, memo: String): Boolean = - withContext(dispatchers.io) { - val walletManager = walletManagersFacade.getOrCreateWalletManager( - userWalletId = userWalletId, - network = network, - ) ?: return@withContext true + override suspend fun validateMemo(network: Network, memo: String): Boolean = + memoValidatorFacade.validateMemo(network, memo) - val memoStateResult = (walletManager as? TransactionValidator)?.validateMemo(memo) - if (memoStateResult != null) { - when (memoStateResult) { - is Result.Success -> when (memoStateResult.data) { - MemoState.NotSupported, - MemoState.Valid, - -> true - MemoState.Invalid -> false - } - is Result.Failure -> true - } - } else { - true - } - } + override suspend fun isMemoRequired(network: Network, destinationAddress: String): Boolean = + memoValidatorFacade.isMemoRequired(network, destinationAddress) override suspend fun parseSharedAddress(input: String, network: Network): ParsedQrCode { val blockchain = network.toBlockchain() diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt index 2a38ea32b4..f3567dbef8 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt @@ -3,15 +3,18 @@ package com.tangem.data.transaction.di import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.transaction.DefaultFeeRepository import com.tangem.data.transaction.DefaultGaslessTransactionRepository +import com.tangem.data.transaction.DefaultMemoValidatorFacade import com.tangem.data.transaction.DefaultTransactionRepository import com.tangem.data.transaction.DefaultWalletAddressServiceRepository import com.tangem.data.transaction.error.DefaultFeeErrorResolver +import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.datasource.api.gasless.GaslessTxServiceApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.GaslessTransactionRepository +import com.tangem.domain.transaction.MemoValidatorFacade import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.transaction.error.FeeErrorResolver @@ -52,14 +55,28 @@ internal object TransactionDataModule { ) } + @Provides + @Singleton + fun providesMemoValidatorFacade( + blockchainSDKFactory: BlockchainSDKFactory, + coroutineDispatcherProvider: CoroutineDispatcherProvider, + ): MemoValidatorFacade { + return DefaultMemoValidatorFacade( + blockchainSDKFactory = blockchainSDKFactory, + dispatchers = coroutineDispatcherProvider, + ) + } + @Provides @Singleton fun providesWalletAddressServiceRepository( walletManagersFacade: WalletManagersFacade, + memoValidatorFacade: MemoValidatorFacade, coroutineDispatcherProvider: CoroutineDispatcherProvider, ): WalletAddressServiceRepository { return DefaultWalletAddressServiceRepository( walletManagersFacade = walletManagersFacade, + memoValidatorFacade = memoValidatorFacade, dispatchers = coroutineDispatcherProvider, ) } 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 906c1ff0a4..e4b068dcd7 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 @@ -11,4 +11,5 @@ data class CryptoCurrencyCheck( val utxoAmountLimit: UtxoAmountLimit?, val isAccountFunded: Boolean, val rentWarning: CryptoCurrencyWarning.Rent?, + val isMemoRequired: Boolean = false, ) \ No newline at end of file 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 8cd50559ec..42fd5e32ad 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 @@ -45,6 +45,14 @@ class GetCurrencyCheckUseCase( } else { false } + val isMemoRequired = if (recipientAddress != null) { + currencyChecksRepository.checkIfMemoRequired( + network = network, + address = recipientAddress, + ) + } else { + false + } val utxoAmountLimit = if (currency is CryptoCurrency.Coin && amount != null && fee != null) { currencyChecksRepository.checkUtxoAmountLimit( userWalletId = userWalletId, @@ -65,6 +73,7 @@ class GetCurrencyCheckUseCase( utxoAmountLimit = utxoAmountLimit, isAccountFunded = isAccountFunded, rentWarning = rentWarning, + isMemoRequired = isMemoRequired, ) } } 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 86f9ee6d7e..7fd520e137 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 @@ -35,6 +35,9 @@ interface CurrencyChecksRepository { /** Returns true if account with [address] was reserved with minimum amount */ suspend fun checkIfAccountFunded(userWalletId: UserWalletId, network: Network, address: String): Boolean + /** Returns true if a memo/destination tag is required for the given [address] on [network] */ + suspend fun checkIfMemoRequired(network: Network, address: String): Boolean + /** Checks if transaction amount is within the UTXO limit */ suspend fun checkUtxoAmountLimit( userWalletId: UserWalletId, diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/MemoValidatorFacade.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/MemoValidatorFacade.kt new file mode 100644 index 0000000000..b29f352b71 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/MemoValidatorFacade.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.transaction + +import com.tangem.domain.models.network.Network + +/** + * Facade for memo validation operations. + */ +interface MemoValidatorFacade { + + /** + * Returns true if a memo/destination tag is required for the given [destinationAddress] on [network]. + * Returns false on network errors or for unsupported blockchains. + */ + suspend fun isMemoRequired(network: Network, destinationAddress: String): Boolean + + /** + * Returns true if [memo] is valid for the given [network], or if memo is not supported. + * Returns true on errors (lenient fallback). + */ + suspend fun validateMemo(network: Network, memo: String): Boolean +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt index e43d756df6..b104323e90 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt @@ -23,7 +23,9 @@ interface WalletAddressServiceRepository { suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean - suspend fun validateMemo(userWalletId: UserWalletId, network: Network, memo: String): Boolean + suspend fun validateMemo(network: Network, memo: String): Boolean + + suspend fun isMemoRequired(network: Network, destinationAddress: String): Boolean suspend fun parseSharedAddress(input: String, network: Network): ParsedQrCode } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt index eb69e7feff..2945b2f2fb 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt @@ -22,7 +22,6 @@ class ValidateWalletMemoUseCase( ): Either { return try { val isValidMemo = walletAddressServiceRepository.validateMemo( - userWalletId = userWalletId, network = cryptoCurrency.network, memo = memo, ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt index 25783c508b..5cf7dc551b 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt @@ -34,6 +34,7 @@ import com.tangem.domain.tokens.GetCurrencyCheckUseCase import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase +import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.domain.utils.convertToSdkAmount import com.tangem.features.send.v2.api.SendNotificationsComponent import com.tangem.features.send.v2.api.SendNotificationsComponent.Params.NotificationData @@ -68,6 +69,7 @@ internal class NotificationsModel @Inject constructor( private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, private val validateTransactionUseCase: ValidateTransactionUseCase, + private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, private val getTronFeeNotificationShowCountUseCase: GetTronFeeNotificationShowCountUseCase, private val incrementNotificationsShowCountUseCase: IncrementNotificationsShowCountUseCase, private val notificationsUpdateTrigger: SendNotificationsUpdateTrigger, @@ -350,6 +352,10 @@ internal class NotificationsModel @Inject constructor( params.callback.onAmountReduceTo(reduceTo) }, ) + addDestinationTagRequiredNotification( + isMemoRequired = currencyCheck.isMemoRequired, + memo = memo, + ) addHighFeeWarningNotification( enteredAmountValue = enteredAmount, cryptoCurrencyStatus = cryptoCurrencyStatus, @@ -370,6 +376,21 @@ internal class NotificationsModel @Inject constructor( addTronNetworkFeesNotification() } + private suspend fun MutableList.addDestinationTagRequiredNotification( + isMemoRequired: Boolean, + memo: String?, + ) { + if (!isMemoRequired || contains(NotificationUM.Error.DestinationTagRequired)) return + val isMemoInvalid = memo.isNullOrEmpty() || validateWalletMemoUseCase( + userWalletId = userWalletId, + cryptoCurrency = currency, + memo = memo, + ).isLeft() + if (isMemoInvalid) { + add(NotificationUM.Error.DestinationTagRequired) + } + } + private suspend fun MutableList.addTronNetworkFeesNotification() { val cryptoCurrency = cryptoCurrencyStatus.currency val isTronToken = cryptoCurrency is CryptoCurrency.Token && diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 8ff57751af..ebcd3e5e91 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1455" +tangemBlockchainSdk = "develop-1461" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-598" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/BlockchainSDKFactory.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/BlockchainSDKFactory.kt index 991f599242..ffe4eec03d 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/BlockchainSDKFactory.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/BlockchainSDKFactory.kt @@ -1,6 +1,7 @@ package com.tangem.blockchainsdk import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.blockchain.common.memo.MemoValidatorFactory /** * Blockchain SDK components factory @@ -14,4 +15,7 @@ interface BlockchainSDKFactory { /** Get [WalletManagerFactory] synchronously */ suspend fun getWalletManagerFactorySync(): WalletManagerFactory? + + /** Get [MemoValidatorFactory] synchronously */ + suspend fun getMemoValidatorFactorySync(): MemoValidatorFactory? } \ No newline at end of file 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 a812c22529..ed8efd0ebe 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 @@ -2,6 +2,7 @@ package com.tangem.blockchainsdk import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.blockchain.common.memo.MemoValidatorFactory import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -32,6 +33,7 @@ internal class DefaultBlockchainSDKFactory( private val mainScope = CoroutineScope(dispatchers.main) private val walletManagerFactory: Flow = createWalletManagerFactory() + private val memoValidatorFactory: Flow = createMemoValidatorFactory() override suspend fun init() { coroutineScope { @@ -41,6 +43,8 @@ internal class DefaultBlockchainSDKFactory( override suspend fun getWalletManagerFactorySync(): WalletManagerFactory? = walletManagerFactory.firstOrNull() + override suspend fun getMemoValidatorFactorySync(): MemoValidatorFactory? = memoValidatorFactory.firstOrNull() + private fun createWalletManagerFactory(): Flow { return combine( flow = flowOf(blockchainSdkConfig), @@ -51,4 +55,14 @@ internal class DefaultBlockchainSDKFactory( // don't use Lazily because some features (WC) require initialized factory on app started .stateIn(scope = mainScope, started = SharingStarted.Eagerly, initialValue = null) } + + private fun createMemoValidatorFactory(): Flow { + return combine( + flow = flowOf(blockchainSdkConfig), + flow2 = blockchainProvidersTypesManager.get(), + ) { config, providerTypes -> + MemoValidatorFactory(config = config, blockchainProviderTypes = providerTypes) + } + .stateIn(scope = mainScope, started = SharingStarted.Eagerly, initialValue = null) + } } \ No newline at end of file From 9bde46f408eef670df3a2eb17e2245d51163b5d1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Mar 2026 11:22:26 +0300 Subject: [PATCH 34/75] Updated on 2026-08-14 --- .../kotlin/com/tangem/screens/SwapTokenPageObject.kt | 4 ---- .../com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt index 21f6b80423..e313cac282 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt @@ -165,10 +165,6 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT) } - val receiveFiatAmountWithPriceImpactWarning: KNode = child { - hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING) - } - val receiveFiatAmountInformationIcon: KNode = child { hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_INFORMATION_ICON) useUnmergedTree = true diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt index ba385763a9..e61314adb3 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt @@ -206,7 +206,7 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { onSwapTokenScreen { waitForIdle() - receiveFiatAmountWithPriceImpactWarning.assertTextContains("%", substring = true) + receiveFiatAmount.assertTextContains("%", substring = true) } } } @@ -288,7 +288,7 @@ class SwapTokenScreenWarningsTest : BaseTestCase() { } } step("Assert fiat amount with warning is displayed") { - onSwapTokenScreen { receiveFiatAmountWithPriceImpactWarning.assertTextContains("%", substring = true) } + onSwapTokenScreen { receiveFiatAmount.assertTextContains("%", substring = true) } } step("Assert receive amount information icon is displayed") { onSwapTokenScreen { receiveFiatAmountInformationIcon.assertIsDisplayed() } From e8b15413d201a86ead15762af32ba88238a150f2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Mar 2026 12:56:24 +0400 Subject: [PATCH 35/75] Updated on 2026-08-14 --- data/payment/.gitignore | 1 + data/payment/build.gradle.kts | 48 +++++++++++++++++++ data/virtual-account/.gitignore | 1 + data/virtual-account/build.gradle.kts | 13 +++++ domain/payment/.gitignore | 1 + domain/payment/build.gradle.kts | 10 ++++ domain/payment/models/.gitignore | 1 + domain/payment/models/build.gradle.kts | 24 ++++++++++ domain/virtual-account/.gitignore | 1 + domain/virtual-account/build.gradle.kts | 13 +++++ domain/virtual-account/models/.gitignore | 1 + .../virtual-account/models/build.gradle.kts | 13 +++++ settings.gradle.kts | 6 +++ 13 files changed, 133 insertions(+) create mode 100644 data/payment/.gitignore create mode 100644 data/payment/build.gradle.kts create mode 100644 data/virtual-account/.gitignore create mode 100644 data/virtual-account/build.gradle.kts create mode 100644 domain/payment/.gitignore create mode 100644 domain/payment/build.gradle.kts create mode 100644 domain/payment/models/.gitignore create mode 100644 domain/payment/models/build.gradle.kts create mode 100644 domain/virtual-account/.gitignore create mode 100644 domain/virtual-account/build.gradle.kts create mode 100644 domain/virtual-account/models/.gitignore create mode 100644 domain/virtual-account/models/build.gradle.kts diff --git a/data/payment/.gitignore b/data/payment/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/payment/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/payment/build.gradle.kts b/data/payment/build.gradle.kts new file mode 100644 index 0000000000..f6ecd6bd5d --- /dev/null +++ b/data/payment/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.ksp) + id("configuration") +} + +android { + namespace = "com.tangem.data.payment" +} + +dependencies { + /** Project - Data */ + implementation(projects.core.error) + implementation(projects.core.error.ext) + implementation(projects.data.common) + implementation(projects.data.wallets) + + /** Project - Domain */ + implementation(projects.domain.payment) + implementation(projects.domain.payment.models) + implementation(projects.domain.wallets) + implementation(projects.domain.models) + implementation(projects.domain.common) + + /** Project - Utils */ + implementation(projects.core.utils) + implementation(projects.domain.legacy) + implementation(projects.libs.blockchainSdk) + + /** Libs - Tangem */ + implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) + implementation(tangemDeps.card.android) + implementation(tangemDeps.hot.core) + implementation(projects.libs.tangemSdkApi) + + /** Libs - Other */ + implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) + implementation(deps.moshi.kotlin) + ksp(deps.moshi.kotlin.codegen) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} diff --git a/data/virtual-account/.gitignore b/data/virtual-account/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/virtual-account/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/virtual-account/build.gradle.kts b/data/virtual-account/build.gradle.kts new file mode 100644 index 0000000000..8b8a6142c4 --- /dev/null +++ b/data/virtual-account/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.ksp) + id("configuration") +} + +android { + namespace = "com.tangem.data.virtualaccount" +} + +dependencies { +} diff --git a/domain/payment/.gitignore b/domain/payment/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/payment/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/payment/build.gradle.kts b/domain/payment/build.gradle.kts new file mode 100644 index 0000000000..4b4e9ae039 --- /dev/null +++ b/domain/payment/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { + /** Project - Domain */ + api(projects.domain.models) + implementation(projects.domain.payment.models) +} \ No newline at end of file diff --git a/domain/payment/models/.gitignore b/domain/payment/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/payment/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/payment/models/build.gradle.kts b/domain/payment/models/build.gradle.kts new file mode 100644 index 0000000000..9903f6cab2 --- /dev/null +++ b/domain/payment/models/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.ksp) + id("configuration") +} + +dependencies { + /** Project - Core */ + implementation(projects.core.error) + + /** Domain models */ + implementation(projects.domain.models) + + /** Libs - Tangem */ + implementation(tangemDeps.card.core) + + /** Libs - Other */ + implementation(deps.moshi.adapters) + implementation(deps.kotlin.serialization) + implementation(deps.jodatime) + implementation(deps.moshi.kotlin) + ksp(deps.moshi.kotlin.codegen) +} diff --git a/domain/virtual-account/.gitignore b/domain/virtual-account/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/virtual-account/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/virtual-account/build.gradle.kts b/domain/virtual-account/build.gradle.kts new file mode 100644 index 0000000000..ff053920b6 --- /dev/null +++ b/domain/virtual-account/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.ksp) + id("configuration") +} + +android { + namespace = "com.tangem.domain.virtualaccount" +} + +dependencies { +} \ No newline at end of file diff --git a/domain/virtual-account/models/.gitignore b/domain/virtual-account/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/virtual-account/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/virtual-account/models/build.gradle.kts b/domain/virtual-account/models/build.gradle.kts new file mode 100644 index 0000000000..d587d7c152 --- /dev/null +++ b/domain/virtual-account/models/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.ksp) + id("configuration") +} + +android { + namespace = "com.tangem.domain.virtualaccount.models" +} + +dependencies { +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 01dadd7270..46f42fb5bc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -354,6 +354,10 @@ include(":domain:transaction:models") include(":domain:analytics") include(":domain:visa") include(":domain:visa:models") +include(":domain:payment") +include(":domain:payment:models") +include(":domain:virtual-account") +include(":domain:virtual-account:models") include(":domain:onboarding") include(":domain:feedback") include(":domain:feedback:models") @@ -410,6 +414,8 @@ include(":data:wallets") include(":data:analytics") include(":data:transaction") include(":data:visa") +include(":data:payment") +include(":data:virtual-account") include(":data:promo") include(":data:onboarding") include(":data:feedback") From f26ad9136130e056d288143155b6dce0153718dc Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Mar 2026 12:19:24 +0200 Subject: [PATCH 36/75] Updated on 2026-08-14 --- .../ui/ds/field/search/TangemSearchField.kt | 5 +- .../ui/src/main/res/drawable/ic_return_24.xml | 9 + features/feed/impl/build.gradle.kts | 1 + .../components/DefaultFeedEntryComponent.kt | 16 +- .../feed/components/FeedEntryChildFactory.kt | 28 +- .../components/earn/DefaultEarnComponent.kt | 4 +- .../DefaultMarketsTokenDetailsComponent.kt | 11 +- .../search/DefaultSearchComponent.kt | 73 +++ .../tangem/features/feed/di/ModelModule.kt | 6 + .../features/feed/model/earn/EarnModel.kt | 2 +- .../feed/model/feed/FeedComponentModel.kt | 10 +- .../feed/model/feed/FeedModelClickIntents.kt | 2 + .../details/MarketsTokenDetailsModel.kt | 5 - .../MarketsListBatchFlowManager.kt | 5 + .../features/feed/model/search/SearchModel.kt | 302 ++++++++++ .../search/state/SearchStateController.kt | 43 ++ .../ApplySearchMarketBatchTransformer.kt | 32 ++ .../state/transformers/SearchUMTransformer.kt | 7 + .../SetSearchResultsLoadingTransformer.kt | 35 ++ .../transformers/UpdateHistoryTransformer.kt | 22 + .../UpdateMarketItemsTransformer.kt | 27 + .../UpdateSearchBarQueryTransformer.kt | 14 + .../UpdateUserAssetsTransformer.kt | 27 + .../tangem/features/feed/ui/EntryContent.kt | 36 +- .../feed/ui/components/FeedSearchBar.kt | 2 +- .../feed/ui/feed/components/NewsBlock.kt | 23 +- .../preview/MarketsTokenDetailsPreview.kt | 10 - .../detailed/state/MarketsTokenDetailsUM.kt | 4 +- .../features/feed/ui/search/SearchContent.kt | 348 +++++++++++ .../ui/search/preview/SearchContentPreview.kt | 538 ++++++++++++++++++ .../feed/ui/search/state/SearchCallbacks.kt | 8 + .../features/feed/ui/search/state/SearchUM.kt | 52 ++ .../utils/EntryContentAnimationTransitions.kt | 117 ++-- 33 files changed, 1693 insertions(+), 131 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_return_24.xml create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/SearchStateController.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/ApplySearchMarketBatchTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/SearchUMTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/SetSearchResultsLoadingTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateHistoryTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateMarketItemsTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateSearchBarQueryTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateUserAssetsTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt index 1635ee15f2..4e2e9ebb42 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt @@ -143,7 +143,7 @@ fun TangemSearchField( innerTextField = innerTextField, focusManager = focusManager, interactionSource = interactionSource, - color = TangemTheme.colors2.field.backgroundDefault, + color = TangemTheme.colors2.button.backgroundSecondary, keyboardController = keyboardController, ) }, @@ -288,10 +288,9 @@ private fun CancelButton( if (state.query.isNotEmpty()) { state.onQueryChange("") } - focusManager.clearFocus() keyboardController?.hide() - state.onActiveChange(false) state.onClearClick() + focusManager.clearFocus() }, ) } diff --git a/core/ui/src/main/res/drawable/ic_return_24.xml b/core/ui/src/main/res/drawable/ic_return_24.xml new file mode 100644 index 0000000000..f63099d1f3 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_return_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts index 2e076661b9..41a5739506 100644 --- a/features/feed/impl/build.gradle.kts +++ b/features/feed/impl/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { implementation(projects.domain.yieldSupply.models) implementation(projects.domain.yieldSupply) implementation(projects.domain.earn) + implementation(projects.domain.search) /* Compose */ implementation(deps.compose.coil) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt index 9538938a8c..2576b72a84 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt @@ -22,7 +22,6 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.news.model.NewsListConfig -import com.tangem.features.feed.components.earn.DefaultEarnComponent import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent @@ -78,7 +77,6 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( screenSource = AnalyticsParam.ScreensSources.Token, ) }, - onMarketOpenClick = { onMarketOpenClick(null) }, ), ), ) @@ -135,14 +133,11 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( } override fun onOpenEarnPage() { - innerRouter.push( - FeedEntryChildFactory.Child.Earn( - params = DefaultEarnComponent.Params( - onBackClick = { onChildBack() }, - onMarketOpenClick = { onMarketOpenClick(null) }, - ), - ), - ) + innerRouter.push(FeedEntryChildFactory.Child.Earn) + } + + override fun openSearch() { + innerRouter.push(FeedEntryChildFactory.Child.Search) } } @@ -244,7 +239,6 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( paginationConfig = null, ) }, - onMarketOpenClick = { clickIntents.onMarketOpenClick(null) }, ), ) FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt index 812d8b8cef..b4dd71046e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -7,15 +7,18 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.navigation.Route import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.features.feed.components.earn.DefaultEarnComponent -import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles -import com.tangem.features.promobanners.api.PromoBannersBlockComponent import com.tangem.features.feed.components.feed.DefaultFeedComponent +import com.tangem.features.feed.components.feed.DefaultFeedComponent.FeedParams import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent import com.tangem.features.feed.components.news.list.DefaultNewsListComponent +import com.tangem.features.feed.components.news.list.DefaultNewsListComponent.Params +import com.tangem.features.feed.components.search.DefaultSearchComponent +import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles +import com.tangem.features.promobanners.api.PromoBannersBlockComponent import kotlinx.serialization.Serializable import javax.inject.Inject @@ -53,7 +56,11 @@ internal class FeedEntryChildFactory @Inject constructor( @Serializable @Immutable - data class Earn(val params: DefaultEarnComponent.Params) : Child + data object Earn : Child + + @Serializable + @Immutable + data object Search : Child } fun createChild( @@ -86,7 +93,7 @@ internal class FeedEntryChildFactory @Inject constructor( Child.NewsList -> { DefaultNewsListComponent( appComponentContext = appComponentContext, - params = DefaultNewsListComponent.Params( + params = Params( onArticleClicked = { currentArticle, prefetchedArticles, paginationConfig -> feedEntryClickIntents.onArticleClick( articleId = currentArticle, @@ -102,7 +109,7 @@ internal class FeedEntryChildFactory @Inject constructor( Child.Feed -> { DefaultFeedComponent( appComponentContext = appComponentContext, - params = DefaultFeedComponent.FeedParams(feedClickIntents = feedEntryClickIntents), + params = FeedParams(feedClickIntents = feedEntryClickIntents), addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent, promoBannersBlockComponentFactory = promoBannersBlockComponentFactory, newPromoBannersFeatureToggles = newPromoBannersFeatureToggles, @@ -111,10 +118,19 @@ internal class FeedEntryChildFactory @Inject constructor( is Child.Earn -> { DefaultEarnComponent( appComponentContext = appComponentContext, - params = child.params, + params = DefaultEarnComponent.Params( + onBackClick = onBackClicked, + onSearchClicked = feedEntryClickIntents::openSearch, + ), addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent, ) } + Child.Search -> DefaultSearchComponent( + appComponentContext = appComponentContext, + params = DefaultSearchComponent.Params( + onBackClick = onBackClicked, + ), + ) } } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt index e5915d7103..b37c5b7dc7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt @@ -35,7 +35,6 @@ import com.tangem.features.feed.components.market.details.portfolio.add.AddToPor import com.tangem.features.feed.model.earn.EarnModel import com.tangem.features.feed.ui.components.FeedSearchBar import com.tangem.features.feed.ui.earn.EarnContent -import kotlinx.serialization.Serializable internal class DefaultEarnComponent( appComponentContext: AppComponentContext, @@ -129,9 +128,8 @@ internal class DefaultEarnComponent( ) } - @Serializable data class Params( val onBackClick: () -> Unit, - val onMarketOpenClick: () -> Unit, + val onSearchClicked: () -> Unit, ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt index 93292cc527..dcf08d159a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt @@ -10,7 +10,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.lifecycle.compose.LifecycleStartEffect @@ -23,6 +22,8 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent +import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.ds.topbar.TangemTopBarType import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalRedesignEnabled @@ -34,7 +35,6 @@ import com.tangem.features.feed.components.market.details.portfolio.api.MarketsP import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnalyticsEvent import com.tangem.features.feed.model.market.details.state.TokenNetworksState -import com.tangem.features.feed.ui.components.FeedSearchBar import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsContent import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsTopBar import kotlinx.coroutines.flow.collectLatest @@ -100,10 +100,7 @@ internal class DefaultMarketsTokenDetailsComponent( val state by model.state.collectAsStateWithLifecycle() val background = LocalMainBottomSheetColor.current.value if (LocalRedesignEnabled.current) { - FeedSearchBar( - isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED, - feedListSearchBar = state.feedListSearchBar, - modifier = Modifier.drawBehind { drawRect(background) }, + TangemTopBar( startContent = { Icon( imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28), @@ -140,6 +137,7 @@ internal class DefaultMarketsTokenDetailsComponent( .padding(TangemTheme.dimens2.x2_5), ) }, + type = TangemTopBarType.BottomSheet, ) } else { MarketsTokenDetailsTopBar( @@ -188,7 +186,6 @@ internal class DefaultMarketsTokenDetailsComponent( val analyticsParams: AnalyticsParams?, val onBackClicked: () -> Unit, val onArticleClick: (articleId: Int, preselectedArticlesId: List) -> Unit, - val onMarketOpenClick: () -> Unit, ) @Serializable diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt new file mode 100644 index 0000000000..b9cf5d8787 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt @@ -0,0 +1,73 @@ +package com.tangem.features.feed.components.search + +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +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.state.BottomSheetState +import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent +import com.tangem.core.ui.ds.field.search.TangemFieldShape +import com.tangem.core.ui.ds.field.search.TangemSearchField +import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.ds.topbar.TangemTopBarType +import com.tangem.features.feed.model.search.SearchModel +import com.tangem.features.feed.ui.search.SearchContent +import com.tangem.features.feed.ui.search.state.SearchCallbacks + +internal class DefaultSearchComponent( + appComponentContext: AppComponentContext, + private val params: Params, +) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext { + + private val model = getOrCreateModel(params = params) + + @Composable + override fun Title(bottomSheetState: State) { + val state by model.state.collectAsStateWithLifecycle() + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(bottomSheetState.value) { + if (bottomSheetState.value == BottomSheetState.EXPANDED) { + focusRequester.requestFocus() + } + } + + TangemTopBar( + type = TangemTopBarType.BottomSheet, + reserveSlotSpace = false, + content = { + TangemSearchField( + state = state.searchBar, + shape = TangemFieldShape.Circle, + focusRequester = focusRequester, + modifier = Modifier.weight(1f), + enabled = bottomSheetState.value == BottomSheetState.EXPANDED, + ) + }, + ) + } + + @Composable + override fun Content(bottomSheetState: State, modifier: Modifier) { + val state by model.state.collectAsStateWithLifecycle() + val searchCallbacks = remember { + SearchCallbacks( + onLoadMore = model::loadMore, + onClearHintsClick = model::clearSearchHistory, + onTextHintClick = model::onTextHintClick, + onResultMarketTokenClick = model::onResultMarketTokenClick, + ) + } + SearchContent( + modifier = modifier, + content = state.content, + searchCallbacks = searchCallbacks, + ) + } + + data class Params( + val onBackClick: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt index 2fee6f7d0b..72864031f8 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt @@ -11,6 +11,7 @@ import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel import com.tangem.features.feed.model.market.list.MarketsListModel import com.tangem.features.feed.model.news.details.NewsDetailsModel import com.tangem.features.feed.model.news.list.NewsListModel +import com.tangem.features.feed.model.search.SearchModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -65,4 +66,9 @@ internal interface ModelModule { @IntoMap @ClassKey(EarnTypeFilterModel::class) fun provideEarnTypeFilterModel(model: EarnTypeFilterModel): Model + + @Binds + @IntoMap + @ClassKey(SearchModel::class) + fun provideSearchModel(model: SearchModel): Model } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index 71a2894d6b..ac71423894 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -325,7 +325,7 @@ internal class EarnModel @Inject constructor( onNetworkFilterClick = ::onNetworkFilterClick, onTypeFilterClick = ::onTypeFilterClick, onScroll = ::onMostlyUsedScrolled, - onSearchBarClicked = params.onMarketOpenClick, + onSearchBarClicked = params.onSearchClicked, ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt index 3252a220e0..33fc8bb216 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -13,6 +13,7 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.DesignFeatureToggles import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -66,6 +67,7 @@ internal class FeedComponentModel @Inject constructor( private val fetchTopEarnTokensUseCase: FetchTopEarnTokensUseCase, private val getTopEarnTokensUseCase: GetTopEarnTokensUseCase, private val appRouter: AppRouter, + private val designFeatureToggles: DesignFeatureToggles, getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, paramsContainer: ParamsContainer, @@ -233,7 +235,7 @@ internal class FeedComponentModel @Inject constructor( currentDate = getCurrentDate(), feedListSearchBar = FeedListSearchBar( placeholderText = resourceReference( - id = if (feedFeatureToggle.isEarnBlockEnabled) { + id = if (designFeatureToggles.isRedesignEnabled) { R.string.markets_search_title_placeholder } else { R.string.markets_search_header_title @@ -241,7 +243,11 @@ internal class FeedComponentModel @Inject constructor( ), onBarClick = { analyticsEventHandler.send(FeedAnalyticsEvent.TokenSearchedClicked()) - params.feedClickIntents.onMarketOpenClick(null) + if (designFeatureToggles.isRedesignEnabled) { + params.feedClickIntents.openSearch() + } else { + params.feedClickIntents.onMarketOpenClick(null) + } }, ), feedListCallbacks = FeedListCallbacks( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt index b9778a2ed6..96ff7cc2f1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt @@ -29,4 +29,6 @@ internal interface FeedModelClickIntents { fun onOpenAllNews() fun onOpenEarnPage() + + fun openSearch() } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt index eb07937be6..b92a688326 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt @@ -53,7 +53,6 @@ import com.tangem.features.feed.model.market.details.converter.TokenMarketInfoCo import com.tangem.features.feed.model.market.details.formatter.* import com.tangem.features.feed.model.market.details.state.QuotesStateUpdater import com.tangem.features.feed.model.market.details.state.TokenNetworksState -import com.tangem.features.feed.ui.feed.state.FeedListSearchBar import com.tangem.features.feed.ui.market.detailed.state.ExchangesBottomSheetContent import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM import com.tangem.lib.crypto.BlockchainUtils @@ -260,10 +259,6 @@ internal class MarketsTokenDetailsModel @Inject constructor( onScroll = {}, ), onShareClick = ::onShareClick, - feedListSearchBar = FeedListSearchBar( - onBarClick = { params.onMarketOpenClick() }, - placeholderText = resourceReference(id = R.string.markets_search_title_placeholder), - ), ), ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/statemanager/MarketsListBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/statemanager/MarketsListBatchFlowManager.kt index b78cb4a6e8..781dceeb6d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/statemanager/MarketsListBatchFlowManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/statemanager/MarketsListBatchFlowManager.kt @@ -75,6 +75,11 @@ internal class MarketsListBatchFlowManager( private val resultBatches = MutableStateFlow(ResultBatches()) private val uiBatches = stateManager.state.map { it.uiBatches } + val rawItems: Flow> + get() = batchFlow.state + .map { state -> state.data.flatMap { batch -> batch.data } } + .distinctUntilChanged() + val uiItems: StateFlow> get() = uiBatches .map { batches -> diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt new file mode 100644 index 0000000000..fb91bf9fd9 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt @@ -0,0 +1,302 @@ +package com.tangem.features.feed.model.search + +import arrow.core.getOrElse +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.search.model.RecentSearchToken +import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase +import com.tangem.domain.search.usecase.GetSearchResultsUseCase +import com.tangem.domain.search.usecase.SaveSearchQueryUseCase +import com.tangem.features.feed.components.search.DefaultSearchComponent +import com.tangem.features.feed.model.market.list.state.MarketsListUM +import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.model.market.list.statemanager.MarketsListBatchFlowManager +import com.tangem.features.feed.model.search.state.SearchStateController +import com.tangem.features.feed.model.search.state.transformers.* +import com.tangem.features.feed.ui.search.state.* +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import javax.inject.Inject + +private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L + +@Suppress("LongParameterList") +@ModelScoped +internal class SearchModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, + getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, + getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getSearchResultsUseCase: GetSearchResultsUseCase, + private val saveSearchQueryUseCase: SaveSearchQueryUseCase, + private val clearSearchHistoryUseCase: ClearSearchHistoryUseCase, + private val stateController: SearchStateController, +) : Model() { + + private val params = paramsContainer.require() + + private val updateQuotesJob = JobHolder() + private val searchResultsJob = JobHolder() + private var shouldShowAllTokensIncludingUnder100k = false + + private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + }.stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) + + private val searchMarketsListManager by lazy { + MarketsListBatchFlowManager( + getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, + currentAppCurrency = Provider { currentAppCurrency.value }, + currentTrendInterval = Provider { MarketsListUM.TrendInterval.H24 }, + currentSortByType = Provider { SortByTypeUM.Rating }, + currentSearchText = Provider { stateController.value.searchBar.query }, + modelScope = modelScope, + dispatchers = dispatchers, + ) + } + + val state: StateFlow get() = stateController.uiState + + init { + initCallbacks() + subscribeToQueryChanges() + subscribeToMarketUiItems() + subscribeToQuotesPolling() + subscribeToAppCurrencyChanges() + loadHistory() + } + + fun loadMore() { + val content = stateController.value.content + if (content is SearchContentUM.Results) { + val market = content.marketTokens + if (market is MarketSearchResultUM.Content) { + searchMarketsListManager.loadMore() + } + } + } + + fun clearSearchHistory() { + modelScope.launch(dispatchers.default) { + clearSearchHistoryUseCase() + } + } + + fun onTextHintClick(text: String) { + stateController.update(UpdateSearchBarQueryTransformer(text)) + } + + fun onResultMarketTokenClick() { + modelScope.launch(dispatchers.default) { + saveSearchQueryUseCase(stateController.value.searchBar.query) + } + } + + private fun initCallbacks() { + stateController.update(object : SearchUMTransformer { + override fun transform(prevState: SearchUM): SearchUM { + return prevState.copy( + searchBar = prevState.searchBar.copy( + onQueryChange = ::onQueryChange, + onActiveChange = ::onActiveChange, + onClearClick = ::onClearClick, + ), + ) + } + }) + } + + private fun onQueryChange(query: String) { + stateController.update(UpdateSearchBarQueryTransformer(query)) + } + + private fun onActiveChange(isActive: Boolean) { + if (!isActive) params.onBackClick() + } + + private fun onClearClick() { + stateController.update(UpdateSearchBarQueryTransformer("")) + } + + private fun subscribeToQueryChanges() { + stateController.uiState + .map { it.searchBar.query.trim() } + .distinctUntilChanged() + .onEach { query -> + shouldShowAllTokensIncludingUnder100k = false + if (query.isEmpty()) { + searchMarketsListManager.clearStateAndStopAllActions() + updateQuotesJob.cancel() + loadHistory() + } else { + stateController.update(SetSearchResultsLoadingTransformer()) + searchMarketsListManager.reload(searchText = query) + subscribeToSearchResults(query) + } + } + .launchIn(modelScope) + } + + private fun subscribeToSearchResults(query: String) { + modelScope.launch { + getSearchResultsUseCase( + query = query, + marketTokens = searchMarketsListManager.rawItems, + ).collectLatest { searchResult -> + val userAssets = searchResult.userAssets.map { entry -> + UserAssetItemUM( + id = "${entry.userWalletId.stringValue}_${entry.accountId.value}" + + "_${entry.currencyStatus.currency.id.value}", + tokenIconUrl = entry.currencyStatus.currency.iconUrl, + tokenName = entry.currencyStatus.currency.name, + tokenSymbol = entry.currencyStatus.currency.symbol, + accountName = entry.accountName.toDisplayString(), + onClick = { + // TODO in [REDACTED_TASK_KEY] while just a stub item. Will be handled in next task. + }, + ) + }.toImmutableList() + stateController.update(UpdateUserAssetsTransformer(userAssets)) + } + }.saveIn(searchResultsJob) + } + + private fun subscribeToQuotesPolling() { + searchMarketsListManager.onLastBatchLoadedSuccess.onEach { batchKey -> + searchMarketsListManager.loadCharts(setOf(batchKey), MarketsListUM.TrendInterval.H24) + modelScope.loadQuotesWithTimer(UPDATE_QUOTES_TIMER_MILLIS) + }.launchIn(modelScope) + } + + private fun subscribeToAppCurrencyChanges() { + currentAppCurrency.drop(1).onEach { + val query = stateController.value.searchBar.query + if (query.isNotEmpty()) { + searchMarketsListManager.reload() + } + }.launchIn(modelScope) + } + + private fun subscribeToMarketUiItems() { + combine( + flow = stateController.uiState.map { it.searchBar.query }.distinctUntilChanged(), + flow2 = searchMarketsListManager.uiItems, + flow3 = searchMarketsListManager.isSearchNotFoundState, + flow4 = searchMarketsListManager.isInInitialLoadingErrorState, + ) { query, uiItems, isSearchNotFound, isInErrorState -> + if (query.isEmpty()) return@combine null + val marketResult = when { + isSearchNotFound -> MarketSearchResultUM.NotFound + isInErrorState -> MarketSearchResultUM.NotFound + uiItems.isEmpty() -> MarketSearchResultUM.Empty + else -> buildMarketContent(uiItems) + } + SearchMarketBatchUiSnapshot( + marketResult = marketResult, + isSearchNotFound = isSearchNotFound, + isInErrorState = isInErrorState, + ) + } + .filterNotNull() + .onEach { snapshot -> + stateController.update(ApplySearchMarketBatchTransformer(snapshot)) + } + .launchIn(modelScope) + } + + private fun buildMarketContent(allItems: ImmutableList): MarketSearchResultUM.Content { + if (shouldShowAllTokensIncludingUnder100k) { + return MarketSearchResultUM.Content(items = allItems) + } + + val filtered = allItems.filter { !it.isUnder100kMarketCap }.toImmutableList() + val hasUnder100k = filtered.size != allItems.size + + return if (hasUnder100k) { + MarketSearchResultUM.Content( + items = filtered, + shouldShowUnder100kNotification = true, + onShowUnder100kClick = { + shouldShowAllTokensIncludingUnder100k = true + stateController.update( + UpdateMarketItemsTransformer( + MarketSearchResultUM.Content(items = allItems), + ), + ) + }, + ) + } else { + MarketSearchResultUM.Content(items = allItems) + } + } + + private fun loadHistory() { + modelScope.launch { + getSearchResultsUseCase(query = "").collectLatest { searchResult -> + val textHints = searchResult.textHints.map { hint -> + TextHintItemUM(text = hint.text) + }.toImmutableList() + + val recentTokens = searchResult.recentTokens.map { token -> + token.toMarketsListItemUM() + }.toImmutableList() + + stateController.update(UpdateHistoryTransformer(textHints, recentTokens)) + } + }.saveIn(searchResultsJob) + } + + private fun RecentSearchToken.toMarketsListItemUM(): MarketsListItemUM { + return MarketsListItemUM( + id = id, + name = name, + currencySymbol = symbol, + iconUrl = imageUrl, + ratingPosition = null, + marketCap = null, + price = MarketsListItemUM.Price(text = ""), + trendPercentText = "", + trendType = PriceChangeType.NEUTRAL, + chartData = null, + isUnder100kMarketCap = false, + stakingRate = null, + updateTimestamp = timestamp, + ) + } + + private fun AccountName.toDisplayString(): String { + return when (this) { + is AccountName.DefaultMain -> "Main" // TODO [REDACTED_TASK_KEY] localize + is AccountName.Custom -> value + } + } + + private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) { + launch { + while (true) { + delay(timeMillis) + searchMarketsListManager.updateQuotes() + } + }.saveIn(updateQuotesJob) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/SearchStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/SearchStateController.kt new file mode 100644 index 0000000000..ceccae14fa --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/SearchStateController.kt @@ -0,0 +1,43 @@ +package com.tangem.features.feed.model.search.state + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.feed.impl.R +import com.tangem.features.feed.model.search.state.transformers.SearchUMTransformer +import com.tangem.features.feed.ui.search.state.SearchContentUM +import com.tangem.features.feed.ui.search.state.SearchUM +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@ModelScoped +internal class SearchStateController @Inject constructor() { + + private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState()) + + val uiState: StateFlow = mutableUiState.asStateFlow() + + val value: SearchUM + get() = uiState.value + + fun update(transformer: SearchUMTransformer) { + mutableUiState.update(function = transformer::transform) + } + + private fun getInitialState(): SearchUM { + return SearchUM( + searchBar = SearchBarUM( + placeholderText = resourceReference(id = R.string.markets_search_title_placeholder), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = {}, + onClearClick = {}, + ), + content = SearchContentUM.InitialEmpty, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/ApplySearchMarketBatchTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/ApplySearchMarketBatchTransformer.kt new file mode 100644 index 0000000000..41dde90476 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/ApplySearchMarketBatchTransformer.kt @@ -0,0 +1,32 @@ +package com.tangem.features.feed.model.search.state.transformers + +import com.tangem.features.feed.ui.search.state.MarketSearchResultUM +import com.tangem.features.feed.ui.search.state.SearchContentUM +import com.tangem.features.feed.ui.search.state.SearchUM + +internal class ApplySearchMarketBatchTransformer( + private val snapshot: SearchMarketBatchUiSnapshot, +) : SearchUMTransformer { + + override fun transform(prevState: SearchUM): SearchUM { + if (shouldIgnoreTransientMarketEmpty(prevState, snapshot)) return prevState + return UpdateMarketItemsTransformer(snapshot.marketResult).transform(prevState) + } + + private fun shouldIgnoreTransientMarketEmpty(prevState: SearchUM, snapshot: SearchMarketBatchUiSnapshot): Boolean { + if (snapshot.marketResult !is MarketSearchResultUM.Empty) return false + if (snapshot.isSearchNotFound || snapshot.isInErrorState) return false + val prev = prevState.content as? SearchContentUM.Results ?: return false + return when (prev.marketTokens) { + is MarketSearchResultUM.Content, is MarketSearchResultUM.Loading -> true + else -> false + } + } +} + +/** Inputs from the search market list flow before merging into [SearchUM]. */ +internal data class SearchMarketBatchUiSnapshot( + val marketResult: MarketSearchResultUM, + val isSearchNotFound: Boolean, + val isInErrorState: Boolean, +) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/SearchUMTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/SearchUMTransformer.kt new file mode 100644 index 0000000000..e39fd493b9 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/SearchUMTransformer.kt @@ -0,0 +1,7 @@ +package com.tangem.features.feed.model.search.state.transformers + +import com.tangem.features.feed.ui.search.state.SearchUM + +internal interface SearchUMTransformer { + fun transform(prevState: SearchUM): SearchUM +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/SetSearchResultsLoadingTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/SetSearchResultsLoadingTransformer.kt new file mode 100644 index 0000000000..480dd0574a --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/SetSearchResultsLoadingTransformer.kt @@ -0,0 +1,35 @@ +package com.tangem.features.feed.model.search.state.transformers + +import com.tangem.features.feed.ui.search.state.MarketSearchResultUM +import com.tangem.features.feed.ui.search.state.SearchContentUM +import com.tangem.features.feed.ui.search.state.SearchUM +import kotlinx.collections.immutable.persistentListOf + +internal class SetSearchResultsLoadingTransformer : SearchUMTransformer { + + override fun transform(prevState: SearchUM): SearchUM { + val currentContent = prevState.content + val currentUserAssets = if (currentContent is SearchContentUM.Results) { + currentContent.userAssets + } else { + persistentListOf() + } + // Keep showing the previous market list while the new query loads (stale-while-revalidate). + // Replacing with Loading + empty batch flashes skeletons / empty section even when the API + // returns the same tokens for a refined query. + val nextMarketTokens = if (currentContent is SearchContentUM.Results) { + when (val market = currentContent.marketTokens) { + is MarketSearchResultUM.Content -> market + else -> MarketSearchResultUM.Loading + } + } else { + MarketSearchResultUM.Loading + } + return prevState.copy( + content = SearchContentUM.Results( + userAssets = currentUserAssets, + marketTokens = nextMarketTokens, + ), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateHistoryTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateHistoryTransformer.kt new file mode 100644 index 0000000000..ec829bf58d --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateHistoryTransformer.kt @@ -0,0 +1,22 @@ +package com.tangem.features.feed.model.search.state.transformers + +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.features.feed.ui.search.state.SearchContentUM +import com.tangem.features.feed.ui.search.state.SearchUM +import com.tangem.features.feed.ui.search.state.TextHintItemUM +import kotlinx.collections.immutable.ImmutableList + +internal class UpdateHistoryTransformer( + private val textHints: ImmutableList, + private val recentTokens: ImmutableList, +) : SearchUMTransformer { + + override fun transform(prevState: SearchUM): SearchUM { + return prevState.copy( + content = SearchContentUM.History( + textHints = textHints, + recentTokens = recentTokens, + ), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateMarketItemsTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateMarketItemsTransformer.kt new file mode 100644 index 0000000000..00e460055f --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateMarketItemsTransformer.kt @@ -0,0 +1,27 @@ +package com.tangem.features.feed.model.search.state.transformers + +import com.tangem.features.feed.ui.search.state.MarketSearchResultUM +import com.tangem.features.feed.ui.search.state.SearchContentUM +import com.tangem.features.feed.ui.search.state.SearchUM +import kotlinx.collections.immutable.persistentListOf + +internal class UpdateMarketItemsTransformer( + private val marketResult: MarketSearchResultUM, +) : SearchUMTransformer { + + override fun transform(prevState: SearchUM): SearchUM { + val currentContent = prevState.content + + val currentUserAssets = if (currentContent is SearchContentUM.Results) { + currentContent.userAssets + } else { + persistentListOf() + } + return prevState.copy( + content = SearchContentUM.Results( + userAssets = currentUserAssets, + marketTokens = marketResult, + ), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateSearchBarQueryTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateSearchBarQueryTransformer.kt new file mode 100644 index 0000000000..f1dc142848 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateSearchBarQueryTransformer.kt @@ -0,0 +1,14 @@ +package com.tangem.features.feed.model.search.state.transformers + +import com.tangem.features.feed.ui.search.state.SearchUM + +internal class UpdateSearchBarQueryTransformer( + private val query: String, +) : SearchUMTransformer { + + override fun transform(prevState: SearchUM): SearchUM { + return prevState.copy( + searchBar = prevState.searchBar.copy(query = query), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateUserAssetsTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateUserAssetsTransformer.kt new file mode 100644 index 0000000000..d99fdf0e7c --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateUserAssetsTransformer.kt @@ -0,0 +1,27 @@ +package com.tangem.features.feed.model.search.state.transformers + +import com.tangem.features.feed.ui.search.state.MarketSearchResultUM +import com.tangem.features.feed.ui.search.state.SearchContentUM +import com.tangem.features.feed.ui.search.state.SearchUM +import com.tangem.features.feed.ui.search.state.UserAssetItemUM +import kotlinx.collections.immutable.ImmutableList + +internal class UpdateUserAssetsTransformer( + private val userAssets: ImmutableList, +) : SearchUMTransformer { + + override fun transform(prevState: SearchUM): SearchUM { + val currentContent = prevState.content + val currentMarketTokens = if (currentContent is SearchContentUM.Results) { + currentContent.marketTokens + } else { + MarketSearchResultUM.Loading + } + return prevState.copy( + content = SearchContentUM.Results( + userAssets = userAssets, + marketTokens = currentMarketTokens, + ), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt index a8efdab5d9..4f1527ad16 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt @@ -1,5 +1,6 @@ package com.tangem.features.feed.ui +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.Scaffold @@ -11,16 +12,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp -import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.router.stack.ChildStack import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.features.feed.components.FeedEntryChildFactory -import com.tangem.features.feed.ui.utils.contentFeedEntryStackAnimation -import com.tangem.features.feed.ui.utils.topBarFeedEntryStackAnimation +import com.tangem.features.feed.ui.utils.contentFeedEntryAnimatedContentTransitionSpec +import com.tangem.features.feed.ui.utils.topBarFeedEntryAnimatedContentTransitionSpec +@OptIn(ExperimentalDecomposeApi::class) @Composable internal fun EntryContent( bottomSheetState: State, @@ -30,15 +32,15 @@ internal fun EntryContent( ) { val density = LocalDensity.current val background = LocalMainBottomSheetColor.current.value - val animationContent = remember { contentFeedEntryStackAnimation() } - val animationAppBar = remember { topBarFeedEntryStackAnimation() } + val animationContent = remember(stackState) { contentFeedEntryAnimatedContentTransitionSpec(stackState) } + val animationAppBar = remember(stackState) { topBarFeedEntryAnimatedContentTransitionSpec(stackState) } Surface(contentColor = background) { Scaffold( containerColor = background, contentWindowInsets = WindowInsetsZero, topBar = { - Children( + AnimatedContent( modifier = Modifier .then( if (!isOpenedInBottomSheet) { @@ -54,18 +56,22 @@ internal fun EntryContent( } } }, - stack = stackState.value, - animation = animationAppBar, - ) { child -> - child.instance.Title(bottomSheetState) + targetState = stackState.value.active, + transitionSpec = animationAppBar, + contentKey = { it.key }, + label = "FeedEntryAppBar", + ) { state -> + state.instance.Title(bottomSheetState) } }, content = { contentPadding -> - Children( - stack = stackState.value, - animation = animationContent, - ) { child -> - child.instance.Content( + AnimatedContent( + targetState = stackState.value.active, + transitionSpec = animationContent, + contentKey = { it.key }, + label = "FeedEntryContent", + ) { state -> + state.instance.Content( modifier = Modifier.padding(contentPadding), bottomSheetState = bottomSheetState, ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/FeedSearchBar.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/FeedSearchBar.kt index 34aa87a805..7d9ab43a62 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/FeedSearchBar.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/FeedSearchBar.kt @@ -107,7 +107,7 @@ private fun FeedSearchBarV2( end = if (endContent != null) TangemTheme.dimens2.x3 else 0.dp, ) .clip(CircleShape) - .background(color = TangemTheme.colors2.field.backgroundDefault) + .background(color = TangemTheme.colors2.button.backgroundSecondary) .conditional(condition = isSearchBarClickable) { clickable(onClick = feedListSearchBar.onBarClick) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt index 4601283941..443eff2ac5 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt @@ -17,11 +17,10 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import com.tangem.features.feed.ui.feed.components.articles.ArticleCard -import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerW @@ -31,6 +30,8 @@ import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.ui.feed.components.articles.ArticleCard +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.features.feed.ui.feed.state.* internal const val FOURTH_ITEM_INDEX = 3 @@ -87,8 +88,16 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, Row(verticalAlignment = Alignment.CenterVertically) { Text( text = stringResourceSafe(R.string.common_news), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, + style = if (isRedesignEnabled) { + TangemTheme.typography2.headingSemibold20 + } else { + TangemTheme.typography.h3 + }, + color = if (isRedesignEnabled) { + TangemTheme.colors2.text.neutral.primary + } else { + TangemTheme.colors.text.primary1 + }, ) SpacerW(4.dp) @@ -113,7 +122,11 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, append(stringResourceSafe(R.string.feed_tangem_ai)) } }, - style = TangemTheme.typography.subtitle1, + style = if (isRedesignEnabled) { + TangemTheme.typography2.bodyRegular16.copy(fontWeight = FontWeight.Medium) + } else { + TangemTheme.typography.subtitle1 + }, overflow = TextOverflow.Ellipsis, maxLines = 1, ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt index 65ce49e61e..ed7261d399 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt @@ -5,10 +5,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.feed.ui.feed.state.FeedListSearchBar import com.tangem.features.feed.ui.market.detailed.state.* import kotlinx.collections.immutable.persistentListOf @@ -52,10 +50,6 @@ internal object MarketsTokenDetailsPreview { onScroll = {}, ), onShareClick = {}, - feedListSearchBar = FeedListSearchBar( - placeholderText = TextReference.Str("Search tokens & news"), - onBarClick = {}, - ), ) val contentState = MarketsTokenDetailsUM( @@ -150,9 +144,5 @@ internal object MarketsTokenDetailsPreview { onScroll = {}, ), onShareClick = {}, - feedListSearchBar = FeedListSearchBar( - placeholderText = TextReference.Str("Search tokens & news"), - onBarClick = {}, - ), ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt index c6e0bde66a..7da2ed065b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt @@ -2,13 +2,12 @@ package com.tangem.features.feed.ui.market.detailed.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.feed.ui.feed.state.FeedListSearchBar +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @@ -30,7 +29,6 @@ internal data class MarketsTokenDetailsUM( val onShouldShowPriceSubtitleChange: (Boolean) -> Unit, val relatedNews: RelatedNews, val onShareClick: () -> Unit, - val feedListSearchBar: FeedListSearchBar, ) { data class ChartState( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt new file mode 100644 index 0000000000..d1f01ad1cc --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt @@ -0,0 +1,348 @@ +package com.tangem.features.feed.ui.search + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.markets.MarketsListItem +import com.tangem.common.ui.markets.MarketsListItemPlaceholder +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.list.InfiniteListHandler +import com.tangem.core.ui.ds.button.* +import com.tangem.core.ui.ds.image.TangemIcon +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.clickableSingle +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.ui.search.state.* + +private const val PLACEHOLDER_COUNT = 10 +private const val LOAD_MORE_THRESHOLD = 5 + +@Composable +internal fun SearchContent(content: SearchContentUM, searchCallbacks: SearchCallbacks, modifier: Modifier = Modifier) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + val lazyListState = rememberLazyListState() + + LazyColumn( + state = lazyListState, + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues( + start = TangemTheme.dimens2.x4, + end = TangemTheme.dimens2.x4, + bottom = bottomBarHeight, + ), + ) { + when (content) { + is SearchContentUM.InitialEmpty -> Unit + is SearchContentUM.History -> searchHistoryItems( + history = content, + onClearAllClick = searchCallbacks.onClearHintsClick, + onHintClick = searchCallbacks.onTextHintClick, + ) + is SearchContentUM.Results -> searchResultsItems( + results = content, + onResultMarketTokenClick = searchCallbacks.onResultMarketTokenClick, + ) + } + } + if (content is SearchContentUM.Results) { + InfiniteListHandler( + listState = lazyListState, + buffer = LOAD_MORE_THRESHOLD, + triggerLoadMoreCheckOnItemsCountChange = true, + onLoadMore = remember(content.marketTokens) { + { + searchCallbacks.onLoadMore() + true + } + }, + ) + } +} + +private fun LazyListScope.searchHistoryItems( + history: SearchContentUM.History, + onClearAllClick: (() -> Unit), + onHintClick: (String) -> Unit, +) { + if (!history.textHints.isEmpty() || !history.recentTokens.isEmpty()) { + item(key = "recents") { + SectionHeader( + title = stringResourceSafe(R.string.markets_search_hint_header), + onClearAllClick = onClearAllClick, + ) + } + } + items( + items = history.textHints, + key = { "hint_${it.text}" }, + ) { hint -> + TextHintItem(hint = hint, onHintClick = { onHintClick(hint.text) }) + HorizontalDivider( + modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x2), + color = TangemTheme.colors2.border.neutral.primary, + ) + } + items( + items = history.recentTokens, + key = { "recent_${it.getComposeKey()}" }, + ) { token -> + MarketsListItem( + modifier = Modifier + .padding(bottom = TangemTheme.dimens2.x2) + .background( + color = TangemTheme.colors2.surface.level3, + shape = RoundedCornerShape(TangemTheme.dimens2.x5), + ), + model = token, + onClick = {}, // TODO in [REDACTED_TASK_KEY] + ) + } +} + +private fun LazyListScope.searchResultsItems(results: SearchContentUM.Results, onResultMarketTokenClick: () -> Unit) { + if (results.userAssets.isNotEmpty()) { + item(key = "header_portfolio") { + SectionHeader(title = stringResourceSafe(R.string.markets_search_portfolio_header)) + } + items( + items = results.userAssets, + key = { it.id }, + ) { asset -> + UserAssetItem(asset) + } + } + + when (val market = results.marketTokens) { + is MarketSearchResultUM.Empty -> Unit + is MarketSearchResultUM.Content -> marketSearchResultItems( + market = market, + hasUserAssetsSection = results.userAssets.isNotEmpty(), + onResultMarketTokenClick = onResultMarketTokenClick, + ) + is MarketSearchResultUM.Loading -> marketSearchResultLoadingItems( + hasUserAssetsSection = results.userAssets.isNotEmpty(), + ) + is MarketSearchResultUM.NotFound -> marketSearchResultNotFoundItem() + } +} + +private fun LazyListScope.marketSearchResultItems( + market: MarketSearchResultUM.Content, + hasUserAssetsSection: Boolean, + onResultMarketTokenClick: () -> Unit, +) { + if (hasUserAssetsSection) { + item(key = "spacer_between_sections") { + SpacerH(TangemTheme.dimens2.x9) + } + } + item(key = "header_market") { + SectionHeader(title = stringResourceSafe(R.string.markets_common_title)) + } + items( + items = market.items, + key = { "market_${it.getComposeKey()}" }, + ) { token -> + MarketsListItem( + modifier = Modifier + .padding(bottom = TangemTheme.dimens2.x2) + .background( + color = TangemTheme.colors2.surface.level3, + shape = RoundedCornerShape(TangemTheme.dimens2.x5), + ), + model = token, + onClick = onResultMarketTokenClick, + ) + } + if (market.shouldShowUnder100kNotification) { + item(key = "show_tokens_under_100k") { + ShowTokensUnder100kItem( + onShowTokensClick = market.onShowUnder100kClick, + ) + } + } +} + +private fun LazyListScope.marketSearchResultLoadingItems(hasUserAssetsSection: Boolean) { + if (hasUserAssetsSection) { + item(key = "spacer_between_sections") { + SpacerH(TangemTheme.dimens2.x9) + } + } + item(key = "header_market") { + SectionHeader(title = stringResourceSafe(R.string.markets_common_title)) + } + items(count = PLACEHOLDER_COUNT) { + MarketsListItemPlaceholder() + } +} + +private fun LazyListScope.marketSearchResultNotFoundItem() { + item(key = "not_found") { + Box( + modifier = Modifier.fillParentMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResourceSafe(R.string.common_no_results), + style = TangemTheme.typography2.bodyRegular14, + color = TangemTheme.colors2.text.neutral.tertiary, + ) + } + } +} + +@Composable +private fun TextHintItem(hint: TextHintItemUM, onHintClick: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onHintClick) + .padding(vertical = 22.dp, horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_search_default_24), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.primary, + modifier = Modifier.size(TangemTheme.dimens2.x5), + ) + SpacerW(TangemTheme.dimens2.x1) + Text( + modifier = Modifier.weight(1f), + text = hint.text, + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + SpacerW(TangemTheme.dimens2.x2) + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_return_24), + contentDescription = null, + tint = TangemTheme.colors2.markers.iconGray, + modifier = Modifier.size(TangemTheme.dimens2.x6), + ) + } +} + +// TODO in [REDACTED_TASK_KEY] while just a stub item. Will be handled in next task. +@Composable +private fun UserAssetItem(asset: UserAssetItemUM) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = asset.onClick) + .padding(horizontal = 12.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + TangemIcon( + tangemIconUM = TangemIconUM.Url(asset.tokenIconUrl, fallbackRes = R.drawable.ic_custom_token_44), + modifier = Modifier + .size(40.dp) + .clip(CircleShape), + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = asset.tokenName, + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + ) + Text( + text = "${asset.tokenSymbol} · ${asset.accountName}", + style = TangemTheme.typography2.captionRegular13, + color = TangemTheme.colors2.text.neutral.tertiary, + maxLines = 1, + ) + } + } +} + +@Composable +private fun ShowTokensUnder100kItem(onShowTokensClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .padding( + top = TangemTheme.dimens2.x9, + bottom = TangemTheme.dimens2.x3, + start = TangemTheme.dimens2.x10, + end = TangemTheme.dimens2.x10, + ) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResourceSafe(R.string.markets_search_see_tokens_under_100k), + style = TangemTheme.typography2.bodyRegular14, + color = TangemTheme.colors2.text.neutral.secondary, + ) + TangemButton( + buttonUM = TangemButtonUM( + text = resourceReference(R.string.markets_search_show_tokens), + onClick = onShowTokensClick, + type = TangemButtonType.Secondary, + size = TangemButtonSize.X8, + shape = TangemButtonShape.Rounded, + ), + ) + } +} + +@Composable +private fun SectionHeader(title: String, modifier: Modifier = Modifier, onClearAllClick: (() -> Unit)? = null) { + Row( + modifier = modifier + .padding( + vertical = TangemTheme.dimens2.x3, + horizontal = TangemTheme.dimens2.x2, + ) + .padding(bottom = TangemTheme.dimens2.x3), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(1f), + text = title, + style = TangemTheme.typography2.headingSemibold20, + color = TangemTheme.colors2.text.neutral.primary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + + if (onClearAllClick != null) { + Text( + modifier = Modifier.clickableSingle(onClick = onClearAllClick), + text = stringResourceSafe(R.string.markets_search_clear_all_hints), + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt new file mode 100644 index 0000000000..227ab6c942 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt @@ -0,0 +1,538 @@ +package com.tangem.features.feed.ui.search.preview + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +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.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.feed.ui.search.SearchContent +import com.tangem.features.feed.ui.search.state.* +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +/** Labeled UI state for [SearchContent] previews. */ +internal data class SearchContentPreviewScenario( + val title: String, + val content: SearchContentUM, +) + +/** Sample market rows, portfolio assets, and hints for [SearchContent] previews (callbacks are no-op). */ +@Suppress("MagicNumber", "StringLiteralDuplication", "LargeClass") +internal object SearchContentPreviewFixtures { + + private val chartYSample = persistentListOf( + 0.4, 0.2, 0.45, 0.35, 0.5, 0.55, 0.48, 0.62, 0.58, 0.7, 0.65, 0.8, + ) + + val scenarioInitialEmpty = SearchContentPreviewScenario( + title = "InitialEmpty – blank screen before input", + content = SearchContentUM.InitialEmpty, + ) + + val scenarioHistoryEmptyBoth = SearchContentPreviewScenario( + title = "History – empty hints and recents (no Recents header)", + content = SearchContentUM.History( + textHints = persistentListOf(), + recentTokens = persistentListOf(), + ), + ) + + val scenarioHistoryHintsOnly = SearchContentPreviewScenario( + title = "History – text hints only", + content = SearchContentUM.History( + textHints = hintsSample(), + recentTokens = persistentListOf(), + ), + ) + + val scenarioHistoryRecentsOnly = SearchContentPreviewScenario( + title = "History – recent tokens only", + content = SearchContentUM.History( + textHints = persistentListOf(), + recentTokens = recentsSample(), + ), + ) + + val scenarioHistoryFull = SearchContentPreviewScenario( + title = "History – hints + recents + Clear all", + content = SearchContentUM.History( + textHints = hintsSample(), + recentTokens = recentsSample(), + ), + ) + + val scenarioResultsMarketEmptyNoPortfolio = SearchContentPreviewScenario( + title = "Results – Market.Empty, empty portfolio", + content = SearchContentUM.Results( + userAssets = persistentListOf(), + marketTokens = MarketSearchResultUM.Empty, + ), + ) + + val scenarioResultsMarketEmptyWithPortfolio = SearchContentPreviewScenario( + title = "Results – Market.Empty, with portfolio", + content = SearchContentUM.Results( + userAssets = portfolioTwo(), + marketTokens = MarketSearchResultUM.Empty, + ), + ) + + val scenarioResultsLoadingNoPortfolio = SearchContentPreviewScenario( + title = "Results – Market.Loading, no portfolio", + content = SearchContentUM.Results( + userAssets = persistentListOf(), + marketTokens = MarketSearchResultUM.Loading, + ), + ) + + val scenarioResultsLoadingWithPortfolio = SearchContentPreviewScenario( + title = "Results – Market.Loading + portfolio (spacer + Market header)", + content = SearchContentUM.Results( + userAssets = portfolioTwo(), + marketTokens = MarketSearchResultUM.Loading, + ), + ) + + val scenarioResultsNotFoundNoPortfolio = SearchContentPreviewScenario( + title = "Results – Market.NotFound, no portfolio", + content = SearchContentUM.Results( + userAssets = persistentListOf(), + marketTokens = MarketSearchResultUM.NotFound, + ), + ) + + val scenarioResultsNotFoundWithPortfolio = SearchContentPreviewScenario( + title = "Results – Market.NotFound + portfolio", + content = SearchContentUM.Results( + userAssets = portfolioTwo(), + marketTokens = MarketSearchResultUM.NotFound, + ), + ) + + val scenarioResultsContentMarketOnly = SearchContentPreviewScenario( + title = "Results – Market.Content, market only", + content = SearchContentUM.Results( + userAssets = persistentListOf(), + marketTokens = MarketSearchResultUM.Content( + items = marketListShort(), + shouldShowUnder100kNotification = false, + onShowUnder100kClick = {}, + ), + ), + ) + + val scenarioResultsContentPortfolioAndMarket = SearchContentPreviewScenario( + title = "Results – portfolio + market (no under 100k)", + content = SearchContentUM.Results( + userAssets = portfolioTwo(), + marketTokens = MarketSearchResultUM.Content( + items = marketListShort(), + shouldShowUnder100kNotification = false, + onShowUnder100kClick = {}, + ), + ), + ) + + val scenarioResultsContentWithUnder100kBanner = SearchContentPreviewScenario( + title = "Results – portfolio + market + under 100k banner", + content = SearchContentUM.Results( + userAssets = portfolioTwo(), + marketTokens = MarketSearchResultUM.Content( + items = marketListShort(), + shouldShowUnder100kNotification = true, + onShowUnder100kClick = {}, + ), + ), + ) + + val scenarioResultsLongMarketScroll = SearchContentPreviewScenario( + title = "Results – long market list (scroll)", + content = SearchContentUM.Results( + userAssets = portfolioTwo(), + marketTokens = MarketSearchResultUM.Content( + items = marketListLong(), + shouldShowUnder100kNotification = true, + onShowUnder100kClick = {}, + ), + ), + ) + + private fun marketToken( + rawId: String, + name: String, + symbol: String, + trend: PriceChangeType = PriceChangeType.UP, + priceText: String = "$98,765.43", + trendText: String = "+2.34%", + marketCap: String? = "$1.2 T", + rating: String? = "1", + chart: MarketChartRawData? = MarketChartRawData(y = chartYSample), + staking: Boolean = false, + updateTimestamp: Long = rawId.hashCode().toLong(), + ): MarketsListItemUM = MarketsListItemUM( + id = CryptoCurrency.RawID(rawId), + name = name, + currencySymbol = symbol, + iconUrl = null, + ratingPosition = rating, + marketCap = marketCap, + price = MarketsListItemUM.Price(text = priceText), + trendPercentText = trendText, + trendType = trend, + chartData = chart, + isUnder100kMarketCap = false, + stakingRate = if (staking) stringReference("APY 4.2%") else null, + updateTimestamp = updateTimestamp, + ) + + private fun userAsset( + id: String, + name: String, + symbol: String, + accountName: String, + iconUrl: String? = null, + ): UserAssetItemUM = UserAssetItemUM( + id = id, + tokenIconUrl = iconUrl, + tokenName = name, + tokenSymbol = symbol, + accountName = accountName, + onClick = {}, + ) + + private fun textHint(text: String): TextHintItemUM = TextHintItemUM(text = text) + + private fun hintsSample(): ImmutableList = persistentListOf( + textHint("bitcoin"), + textHint("sol"), + textHint("very long search query example for ellipsis"), + ) + + private fun recentsSample(): ImmutableList = persistentListOf( + marketToken(rawId = "btc_r", name = "Bitcoin", symbol = "BTC"), + marketToken( + rawId = "eth_r", + name = "Ethereum", + symbol = "ETH", + trend = PriceChangeType.DOWN, + trendText = "−1.02%", + rating = "2", + ), + marketToken( + rawId = "long_r", + name = "A Very Long Token Name That Should Ellipsize In The List", + symbol = "LONG", + trend = PriceChangeType.NEUTRAL, + trendText = "0.00%", + marketCap = "$999.123456789 B", + rating = "42", + ), + ) + + private fun portfolioTwo(): ImmutableList = persistentListOf( + userAsset(id = "p1", name = "Ethereum", symbol = "ETH", accountName = "Main wallet"), + userAsset( + id = "p2", + name = "Polygon", + symbol = "POL", + accountName = "Account with a long label for preview", + ), + ) + + private fun marketListShort(): ImmutableList = persistentListOf( + marketToken(rawId = "m1", name = "Bitcoin", symbol = "BTC", rating = "1"), + marketToken( + rawId = "m2", + name = "Ethereum", + symbol = "ETH", + trend = PriceChangeType.DOWN, + trendText = "−0.55%", + rating = "2", + ), + marketToken( + rawId = "m3", + name = "Solana", + symbol = "SOL", + trend = PriceChangeType.NEUTRAL, + trendText = "0.12%", + rating = "3", + ), + ) + + @Suppress("LongMethod") + private fun marketListLong(): ImmutableList = listOf( + marketToken( + rawId = "L1", + name = "Arbitrum", + symbol = "ARB", + trend = PriceChangeType.UP, + priceText = "$1.12", + trendText = "+8.1%", + marketCap = "$3.1 B", + rating = "10", + ), + marketToken( + rawId = "L2", + name = "Optimism", + symbol = "OP", + trend = PriceChangeType.DOWN, + priceText = "$2.34", + trendText = "−3.2%", + marketCap = "$2.8 B", + rating = "11", + ), + marketToken( + rawId = "L3", + name = "Base", + symbol = "—", + trend = PriceChangeType.NEUTRAL, + priceText = "$0.98", + trendText = "0.0%", + marketCap = "$1.9 B", + rating = "12", + chart = null, + ), + marketToken( + rawId = "L4", + name = "Avalanche", + symbol = "AVAX", + trend = PriceChangeType.UP, + priceText = "$36.5", + trendText = "+4.4%", + marketCap = "$14 B", + rating = "13", + staking = true, + ), + marketToken( + rawId = "L5", + name = "Polkadot", + symbol = "DOT", + trend = PriceChangeType.DOWN, + priceText = "$6.12", + trendText = "−2.1%", + marketCap = "$8 B", + rating = "14", + ), + marketToken( + rawId = "L6", + name = "Cosmos", + symbol = "ATOM", + trend = PriceChangeType.UP, + priceText = "$8.90", + trendText = "+1.1%", + marketCap = "$3.4 B", + rating = "15", + ), + marketToken( + rawId = "L7", + name = "Near", + symbol = "NEAR", + trend = PriceChangeType.DOWN, + priceText = "$4.56", + trendText = "−0.8%", + marketCap = "$4.5 B", + rating = "16", + ), + marketToken( + rawId = "L8", + name = "Sui", + symbol = "SUI", + trend = PriceChangeType.UP, + priceText = "$2.10", + trendText = "+12.3%", + marketCap = "$2.2 B", + rating = "17", + ), + ).toImmutableList() + + fun allScenarios(): List = listOf( + scenarioInitialEmpty, + scenarioHistoryEmptyBoth, + scenarioHistoryHintsOnly, + scenarioHistoryRecentsOnly, + scenarioHistoryFull, + scenarioResultsMarketEmptyNoPortfolio, + scenarioResultsMarketEmptyWithPortfolio, + scenarioResultsLoadingNoPortfolio, + scenarioResultsLoadingWithPortfolio, + scenarioResultsNotFoundNoPortfolio, + scenarioResultsNotFoundWithPortfolio, + scenarioResultsContentMarketOnly, + scenarioResultsContentPortfolioAndMarket, + scenarioResultsContentWithUnder100kBanner, + scenarioResultsLongMarketScroll, + ) +} + +private val SearchContentPreviewCallbacks = SearchCallbacks( + onLoadMore = {}, + onClearHintsClick = {}, + onTextHintClick = { _ -> }, + onResultMarketTokenClick = {}, +) + +/** All [SearchContentPreviewScenario] values for the Preview Parameter dropdown in Android Studio. */ +internal class SearchContentPreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence + get() = SearchContentPreviewFixtures.allScenarios().asSequence() +} + +@Composable +private fun SearchContentPreviewHost( + scenario: SearchContentPreviewScenario, + modifier: Modifier = Modifier, + previewHeightDp: Int = 720, +) { + Box( + modifier = modifier + .height(previewHeightDp.dp) + .fillMaxWidth() + .background(TangemTheme.colors.background.primary), + ) { + SearchContent( + content = scenario.content, + searchCallbacks = SearchContentPreviewCallbacks, + modifier = Modifier.fillMaxSize(), + ) + } +} + +// region Named previews (quick access without cycling parameters) + +@Composable +@Preview(name = "01 Initial empty", showBackground = true, widthDp = 360, heightDp = 720) +@Preview( + name = "01 Initial empty (night)", + showBackground = true, + widthDp = 360, + heightDp = 720, + uiMode = Configuration.UI_MODE_NIGHT_YES, +) +private fun SearchContentPreview_InitialEmpty() { + TangemThemePreviewRedesign { + SearchContentPreviewHost(scenario = SearchContentPreviewFixtures.scenarioInitialEmpty) + } +} + +@Composable +@Preview(name = "02 History full", showBackground = true, widthDp = 360, heightDp = 720) +@Preview( + name = "02 History full (night)", + showBackground = true, + widthDp = 360, + heightDp = 720, + uiMode = Configuration.UI_MODE_NIGHT_YES, +) +private fun SearchContentPreview_HistoryFull() { + TangemThemePreviewRedesign { + SearchContentPreviewHost(scenario = SearchContentPreviewFixtures.scenarioHistoryFull) + } +} + +@Composable +@Preview(name = "03 Results loading + portfolio", showBackground = true, widthDp = 360, heightDp = 720) +@Preview( + name = "03 Results loading + portfolio (night)", + showBackground = true, + widthDp = 360, + heightDp = 720, + uiMode = Configuration.UI_MODE_NIGHT_YES, +) +private fun SearchContentPreview_ResultsLoadingWithPortfolio() { + TangemThemePreviewRedesign { + SearchContentPreviewHost(scenario = SearchContentPreviewFixtures.scenarioResultsLoadingWithPortfolio) + } +} + +@Composable +@Preview(name = "04 Results not found", showBackground = true, widthDp = 360, heightDp = 720) +@Preview( + name = "04 Results not found (night)", + showBackground = true, + widthDp = 360, + heightDp = 720, + uiMode = Configuration.UI_MODE_NIGHT_YES, +) +private fun SearchContentPreview_ResultsNotFound() { + TangemThemePreviewRedesign { + SearchContentPreviewHost(scenario = SearchContentPreviewFixtures.scenarioResultsNotFoundNoPortfolio) + } +} + +@Composable +@Preview(name = "05 Results market + under 100k", showBackground = true, widthDp = 360, heightDp = 800) +@Preview( + name = "05 Results market + under 100k (night)", + showBackground = true, + widthDp = 360, + heightDp = 800, + uiMode = Configuration.UI_MODE_NIGHT_YES, +) +private fun SearchContentPreview_ResultsWithUnder100kBanner() { + TangemThemePreviewRedesign { + SearchContentPreviewHost( + scenario = SearchContentPreviewFixtures.scenarioResultsContentWithUnder100kBanner, + previewHeightDp = 800, + ) + } +} + +@Composable +@Preview(name = "06 Results long scroll", showBackground = true, widthDp = 360, heightDp = 640) +@Preview( + name = "06 Results long scroll (night)", + showBackground = true, + widthDp = 360, + heightDp = 640, + uiMode = Configuration.UI_MODE_NIGHT_YES, +) +private fun SearchContentPreview_ResultsLongList() { + TangemThemePreviewRedesign { + SearchContentPreviewHost( + scenario = SearchContentPreviewFixtures.scenarioResultsLongMarketScroll, + previewHeightDp = 640, + ) + } +} + +// endregion + +/** All scenarios via Preview Parameter; scenario label is the [SearchContentPreviewScenario] title property. */ +@Composable +@Preview(name = "All scenarios (parameter)", showBackground = true, widthDp = 360, heightDp = 720) +@Preview( + name = "All scenarios (parameter, night)", + showBackground = true, + widthDp = 360, + heightDp = 720, + uiMode = Configuration.UI_MODE_NIGHT_YES, +) +private fun SearchContentPreview_AllScenarios( + @PreviewParameter(SearchContentPreviewParameterProvider::class) scenario: SearchContentPreviewScenario, +) { + TangemThemePreviewRedesign { + SearchContentPreviewHost( + scenario = scenario, + previewHeightDp = when (scenario.title) { + SearchContentPreviewFixtures.scenarioResultsLongMarketScroll.title -> 640 + SearchContentPreviewFixtures.scenarioResultsContentWithUnder100kBanner.title -> 800 + else -> 720 + }, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt new file mode 100644 index 0000000000..e793512ce1 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt @@ -0,0 +1,8 @@ +package com.tangem.features.feed.ui.search.state + +internal data class SearchCallbacks( + val onLoadMore: () -> Unit, + val onClearHintsClick: () -> Unit, + val onTextHintClick: (hint: String) -> Unit, + val onResultMarketTokenClick: () -> Unit, +) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt new file mode 100644 index 0000000000..e4628ad30c --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt @@ -0,0 +1,52 @@ +package com.tangem.features.feed.ui.search.state + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import kotlinx.collections.immutable.ImmutableList + +data class SearchUM( + val searchBar: SearchBarUM, + val content: SearchContentUM, +) + +@Immutable +sealed interface SearchContentUM { + + data class History( + val textHints: ImmutableList, + val recentTokens: ImmutableList, + ) : SearchContentUM + + data class Results( + val userAssets: ImmutableList, + val marketTokens: MarketSearchResultUM, + ) : SearchContentUM + + data object InitialEmpty : SearchContentUM +} + +@Immutable +sealed interface MarketSearchResultUM { + + data class Content( + val items: ImmutableList, + val shouldShowUnder100kNotification: Boolean = false, + val onShowUnder100kClick: () -> Unit = {}, + ) : MarketSearchResultUM + + data object Loading : MarketSearchResultUM + data object NotFound : MarketSearchResultUM + data object Empty : MarketSearchResultUM +} + +data class TextHintItemUM(val text: String) + +data class UserAssetItemUM( + val id: String, + val tokenIconUrl: String?, + val tokenName: String, + val tokenSymbol: String, + val accountName: String, + val onClick: () -> Unit, +) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt index 638e28ca11..02de097c9a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt @@ -1,71 +1,70 @@ package com.tangem.features.feed.ui.utils +import androidx.compose.animation.* import androidx.compose.animation.core.tween -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.layout.layout -import androidx.compose.ui.unit.dp -import com.arkivanov.decompose.FaultyDecomposeApi -import com.arkivanov.decompose.extensions.compose.stack.animation.* +import androidx.compose.runtime.State +import com.arkivanov.decompose.Child +import com.arkivanov.decompose.router.stack.ChildStack import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent -import com.tangem.core.ui.utils.toPx import com.tangem.features.feed.components.FeedEntryChildFactory -private const val DELAY_FOR_TRANSITION = 400 +private fun FeedEntryChildFactory.Child?.usesFadeStackTransition(): Boolean = when (this) { + is FeedEntryChildFactory.Child.Search -> true + is FeedEntryChildFactory.Child.TokenList -> params.shouldAlwaysShowSearchBar + else -> false +} -@OptIn(FaultyDecomposeApi::class) -internal fun topBarFeedEntryStackAnimation(): StackAnimation< - FeedEntryChildFactory.Child, - ComposableModularBottomSheetContentComponent, - > = - stackAnimation { to, from, _ -> - val isSearchToTokenList = - (to.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true - val isFromSearchTokenList = - (from.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true - if (isSearchToTokenList || isFromSearchTokenList) { - fade() - } else { - slide() - } +internal typealias FeedEntryActiveChild = + Child.Created + +internal typealias FeedEntryChildStack = + ChildStack + +internal fun topBarFeedEntryAnimatedContentTransitionSpec( + stackState: State, +): AnimatedContentTransitionScope.() -> ContentTransform = + { feedEntryAnimatedContentTransform(stackState.value) } + +internal fun contentFeedEntryAnimatedContentTransitionSpec( + stackState: State, +): AnimatedContentTransitionScope.() -> ContentTransform = + { feedEntryAnimatedContentTransform(stackState.value) } + +private fun AnimatedContentTransitionScope.feedEntryAnimatedContentTransform( + stack: FeedEntryChildStack, +): ContentTransform { + val shouldUseFade = initialState.configuration.usesFadeStackTransition() || + targetState.configuration.usesFadeStackTransition() + return if (shouldUseFade) { + fadeIn(animationSpec = tween(FEED_ENTRY_FADE_DURATION_MS)) togetherWith + fadeOut(animationSpec = tween(FEED_ENTRY_FADE_DURATION_MS)) + } else { + feedEntrySlideTransform(stack) } +} -@OptIn(FaultyDecomposeApi::class) -internal fun contentFeedEntryStackAnimation(): StackAnimation< - FeedEntryChildFactory.Child, - ComposableModularBottomSheetContentComponent, - > = - stackAnimation { to, from, _ -> - val isSearchToTokenList = - (to.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true - val isFromSearchTokenList = - (from.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true - if (isSearchToTokenList || isFromSearchTokenList) { - fadeAndSlideInt() - } else { - slide() - } - } - -private fun fadeAndSlideInt(): StackAnimator = - stackAnimator(animationSpec = tween(DELAY_FOR_TRANSITION)) { factor, _, content -> - val alpha = 1f - kotlin.math.abs(factor) - val slidePx = 32.dp.toPx() - val yOffset = when { - factor > 0 -> (slidePx * factor).toInt() - factor < 0 -> (slidePx * factor * -1).toInt() - else -> 0 - } - content( - Modifier - .alpha(alpha) - .offsetY(yOffset), +private fun AnimatedContentTransitionScope.feedEntrySlideTransform( + stack: FeedEntryChildStack, +): ContentTransform { + val isPushing = stack.backStack.lastOrNull()?.configuration == initialState.configuration + return if (isPushing) { + slideInHorizontally( + animationSpec = tween(FEED_ENTRY_SLIDE_DURATION_MS), + initialOffsetX = { it }, + ) togetherWith slideOutHorizontally( + animationSpec = tween(FEED_ENTRY_SLIDE_DURATION_MS), + targetOffsetX = { -it }, + ) + } else { + slideInHorizontally( + animationSpec = tween(FEED_ENTRY_SLIDE_DURATION_MS), + initialOffsetX = { -it }, + ) togetherWith slideOutHorizontally( + animationSpec = tween(FEED_ENTRY_SLIDE_DURATION_MS), + targetOffsetX = { it }, ) } +} -private fun Modifier.offsetY(pixels: Int): Modifier = layout { measurable, constraints -> - val placeable = measurable.measure(constraints) - layout(placeable.width, placeable.height) { - placeable.placeRelative(x = 0, y = pixels) - } -} \ No newline at end of file +private const val FEED_ENTRY_FADE_DURATION_MS = 300 +private const val FEED_ENTRY_SLIDE_DURATION_MS = 300 \ No newline at end of file From c0fa00479b44a7b8c239e75a0060483f504b6a7e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Mar 2026 14:20:20 +0300 Subject: [PATCH 37/75] Updated on 2026-08-14 --- .../core/ui/ds/row/header/TangemHeaderRow.kt | 1 - .../tangem/core/ui/ds/topbar/TangemTopBar.kt | 4 +- ...BalanceExitUntilCollapsedScrollBehavior.kt | 59 ++++++++++-- .../entity/TangemCollapsingAppBarState.kt | 32 +++++-- .../model/TangemPayWalletSelectorModel.kt | 6 +- .../converter/WalletTokensListUMConverter.kt | 9 +- .../presentation/wallet/ui/WalletScreen2.kt | 94 +++++++++++-------- .../ui/components/common/WalletContent.kt | 62 ++++++------ 8 files changed, 170 insertions(+), 97 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt index 64d73f459d..dee6b40952 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt @@ -117,7 +117,6 @@ fun TangemHeaderRow( * @param modifier Modifier for the composable * @param subtitle Optional subtitle as a TextReference * @param headTangemIconUM Optional TangemIconUM for the head icon - * @param footerTangemIconRes Optional drawable resource ID for the footer icon * @param isEnabled Boolean indicating if the row is clickable * @param onItemClick Optional click callback for the row */ diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt index c05ffb7daa..f74bc80dd8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt @@ -171,7 +171,9 @@ fun TangemTopBar( if (reserveSlotSpace || endContent != null) { AnimatedContent( targetState = endContent != null, - modifier = Modifier.size(TangemTheme.dimens2.x11), + modifier = Modifier + .height(TangemTheme.dimens2.x11) + .widthIn(min = TangemTheme.dimens2.x11), label = "End Content Visibility", ) { isVisible -> if (isVisible) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt index 166348b94b..6ac6a4743f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt @@ -17,7 +17,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState -import com.tangem.core.ui.ds.topbar.collapsing.entity.TopBapScrollDirection +import com.tangem.core.ui.ds.topbar.collapsing.entity.TopBarScrollDirection import com.tangem.core.ui.ds.topbar.collapsing.entity.rememberTangemCollapsingAppBarState import com.tangem.core.ui.utils.toPx import kotlin.math.abs @@ -37,6 +37,7 @@ import kotlin.math.absoluteValue */ @Composable fun rememberTangemExitUntilCollapsedScrollBehavior( + isTopOverscrollEnabled: Boolean = true, expandedHeight: Dp = -Int.MAX_VALUE.dp, partialCollapsedHeight: Dp = expandedHeight, snapAnimationSpec: AnimationSpec? = spring(), @@ -45,6 +46,7 @@ fun rememberTangemExitUntilCollapsedScrollBehavior( val topBarState = rememberTangemCollapsingAppBarState( heightOffsetLimit = -expandedHeight.toPx(), partialHeightLimit = partialCollapsedHeight.toPx(), + isTopOverscrollEnabled = isTopOverscrollEnabled, ) return exitUntilCollapsedScrollBehavior( state = topBarState, @@ -76,7 +78,7 @@ private fun exitUntilCollapsedScrollBehavior( val dy = available.y val consume = if (dy < 0) { - state.direction = TopBapScrollDirection.Collapsing + state.direction = TopBarScrollDirection.Collapsing state.dispatchRawDelta(dy) } else { 0f @@ -89,16 +91,57 @@ private fun exitUntilCollapsedScrollBehavior( val dy = available.y val consume = if (dy > 0) { - state.direction = TopBapScrollDirection.Expanding + state.direction = TopBarScrollDirection.Expanding state.dispatchRawDelta(dy) } else { - state.direction = TopBapScrollDirection.Collapsing + state.direction = TopBarScrollDirection.Collapsing 0f } return Offset(0f, consume) } + @Suppress("MagicNumber") + override suspend fun onPreFling(available: Velocity): Velocity { + val vy = available.y + // Only handle upward fling (collapsing) + if (vy >= 0f) return Velocity.Zero + + val effectiveLimit = if (state.isTopOverscrollEnabled) { + state.heightOffsetLimit + state.partialHeightLimit + } else { + state.heightOffsetLimit + } + + // Already at the collapse limit — nothing to consume + if (state.heightOffset <= effectiveLimit) return Velocity.Zero + + state.direction = TopBarScrollDirection.Collapsing + var remainingVelocity = vy + + if (flingAnimationSpec != null) { + var lastValue = 0f + AnimationState( + initialValue = 0f, + initialVelocity = vy, + ).animateDecay(flingAnimationSpec) { + val delta = value - lastValue + val prevOffset = state.heightOffset + state.heightOffset = + (prevOffset + delta).coerceAtLeast(effectiveLimit) + val consumed = abs(prevOffset - state.heightOffset) + lastValue = value + remainingVelocity = this.velocity + // Stop when the bar can't collapse any further + if (consumed < 0.5f && abs(delta) > 0.5f) { + cancelAnimation() + } + } + } + + return Velocity(0f, available.y - remainingVelocity) + } + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { val superConsumed = super.onPostFling(consumed, available) return superConsumed + settleAppBar( @@ -179,7 +222,7 @@ private suspend fun settleAppBar( val availableDelta = partialLimit - initialHeightOffset - state.heightOffset = if (delta < 0f && initialHeightOffset > partialLimit) { + state.heightOffset = if (delta < 0f && initialHeightOffset >= partialLimit) { (initialHeightOffset + delta).coerceAtLeast(partialLimit) } else { initialHeightOffset + delta @@ -196,17 +239,17 @@ private suspend fun settleAppBar( if (snapAnimationSpec != null && state.heightOffset > partialLimit && state.heightOffset < 0f) { AnimationState(initialValue = state.heightOffset).animateTo( when (state.direction) { - TopBapScrollDirection.Collapsing -> if (state.collapsedFraction > snapCollapseThreshold) { + TopBarScrollDirection.Collapsing -> if (state.collapsedFraction > snapCollapseThreshold) { partialLimit } else { 0f } - TopBapScrollDirection.Expanding -> if (state.collapsedFraction < snapExpandThreshold) { + TopBarScrollDirection.Expanding -> if (state.collapsedFraction < snapExpandThreshold) { 0f } else { partialLimit } - TopBapScrollDirection.Idle -> 0f + TopBarScrollDirection.Idle -> 0f }, animationSpec = snapAnimationSpec, ) { diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt index a5d1a8892d..025b8262f8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt @@ -30,6 +30,7 @@ class TangemCollapsingAppBarState( val initialHeightOffset: Float = 0f, val heightOffsetLimit: Float = 0f, val partialHeightLimit: Float = heightOffsetLimit, + var isTopOverscrollEnabled: Boolean = true, ) : ScrollableState { private val _heightOffset = mutableFloatStateOf(initialHeightOffset) @@ -42,8 +43,7 @@ class TangemCollapsingAppBarState( var heightOffset: Float get() = _heightOffset.floatValue set(newOffset) { - _heightOffset.floatValue = - newOffset.coerceIn(minimumValue = heightOffsetLimit, maximumValue = 0f) + _heightOffset.floatValue = newOffset.coerceIn(minimumValue = heightOffsetLimit, maximumValue = 0f) } /** @@ -60,11 +60,13 @@ class TangemCollapsingAppBarState( /** * The current scroll direction of the app bar, which can be Collapsing, Expanding, or Idle. */ - var direction: TopBapScrollDirection = TopBapScrollDirection.Idle + var direction: TopBarScrollDirection = TopBarScrollDirection.Idle private val scrollableState = ScrollableState { value -> + val effectiveLimit = if (isTopOverscrollEnabled) heightOffsetLimit + partialHeightLimit else heightOffsetLimit val consume = if (value < 0) { - max(heightOffsetLimit - heightOffset, value) + // Already at or past the effective limit — don't consume collapsing scroll + if (heightOffset <= effectiveLimit) 0f else max(effectiveLimit - heightOffset, value) } else { min(0f - heightOffset, value) } @@ -104,12 +106,20 @@ class TangemCollapsingAppBarState( /** The default [Saver] implementation for [TangemCollapsingAppBarState]. */ val Saver: Saver = listSaver( - save = { state -> listOf(state.heightOffsetLimit, state.heightOffset, state.partialHeightLimit) }, + save = { state -> + listOf( + state.heightOffsetLimit, + state.heightOffset, + state.partialHeightLimit, + state.isTopOverscrollEnabled, + ) + }, restore = { state -> TangemCollapsingAppBarState( - heightOffsetLimit = state[0], - partialHeightLimit = state[2], - initialHeightOffset = state[1], + heightOffsetLimit = state[0] as Float, + initialHeightOffset = state[1] as Float, + partialHeightLimit = state[2] as Float, + isTopOverscrollEnabled = state[3] as Boolean, ) }, ) @@ -121,6 +131,7 @@ class TangemCollapsingAppBarState( */ @Composable fun rememberTangemCollapsingAppBarState( + isTopOverscrollEnabled: Boolean = true, heightOffsetLimit: Float = -Float.MAX_VALUE, partialHeightLimit: Float = -Float.MAX_VALUE, initialHeightOffset: Float = 0f, @@ -130,13 +141,16 @@ fun rememberTangemCollapsingAppBarState( initialHeightOffset = initialHeightOffset, partialHeightLimit = partialHeightLimit, heightOffsetLimit = heightOffsetLimit, + isTopOverscrollEnabled = isTopOverscrollEnabled, ) + }.also { + it.isTopOverscrollEnabled = isTopOverscrollEnabled } } /** * The scroll direction of the top app bar, which can be Collapsing, Expanding, or Idle. */ -enum class TopBapScrollDirection { +enum class TopBarScrollDirection { Collapsing, Expanding, Idle } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayWalletSelectorModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayWalletSelectorModel.kt index 27a15aeeeb..5846a5ad8f 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayWalletSelectorModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayWalletSelectorModel.kt @@ -37,13 +37,13 @@ internal class TangemPayWalletSelectorModel @Inject constructor( onWalletClick = { params.listener.onWalletSelected(it) }, ) + val uiState: StateFlow + field = MutableStateFlow(getInitialState()) + init { fetchUserWalletsUM() } - val uiState: StateFlow - field = MutableStateFlow(getInitialState()) - private fun getInitialState(): WalletSelectorBSContentUM { return WalletSelectorBSContentUM( userWallets = persistentListOf(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt index ac40b2a184..aa24a27e24 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt @@ -76,20 +76,17 @@ internal class WalletTokensListUMConverter( onEmptyClick = { clickIntents.onManageTokensClick(value.mainAccount.accountId) }, ) } else { - val isCollapsable = value.accountStatuses.count { - it is AccountStatus.CryptoPortfolio && it.account.tokensCount > 0 - } > 1 - val tokenListUM = value.accountStatuses .filterIsInstance() .asSequence() .flatMap { accountStatus -> if (isAccountsModeEnabled) { + val isCollapsable = accountStatus.tokenList.flattenCurrencies().isNotEmpty() val isExpanded = expandedAccounts.contains(accountStatus.account.accountId) sequenceOf( TokensListItemUM2.Portfolio( tokenRowUM = accountRowConverter.convert(accountStatus), - isExpanded = isExpanded || !isCollapsable, + isExpanded = isExpanded, isCollapsable = isCollapsable, onEmptyClick = { clickIntents.onManageTokensClick(accountStatus.account.accountId) }, tokenList = getTokenListItems( @@ -166,7 +163,7 @@ internal class WalletTokensListUMConverter( return if (accountList.flattenCurrencies().size > 1 && !selectedWallet.isSingleWalletWithToken()) { TangemButtonUM( text = resourceReference(R.string.organize_tokens_title), - isEnabled = accountList.totalFiatBalance is TotalFiatBalance.Loading, + isEnabled = accountList.totalFiatBalance !is TotalFiatBalance.Loading, size = TangemButtonSize.X9, shape = TangemButtonShape.Rounded, type = TangemButtonType.PrimaryInverse, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt index cfa0ea99a6..a8c9e5a8c0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt @@ -58,6 +58,7 @@ import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingTopBar import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.* +import com.tangem.core.ui.utils.TangemSharedTransitionLayout import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM @@ -95,12 +96,28 @@ internal fun WalletScreen2( pageCount = { state.wallets2.size }, ) + val listStates = rememberSaveable(saver = lazyListStateMapSaver(walletsPagerState.pageCount)) { + mutableMapOf().apply { + repeat(walletsPagerState.pageCount) { index -> put(index, LazyListState()) } + } + } + + val isTopOverscrollEnabled by remember { + derivedStateOf { + val listState = listStates[walletsPagerState.currentPage] ?: return@derivedStateOf false + listState.layoutInfo.totalItemsCount > 0 && + !listState.canScrollBackward && !listState.canScrollForward || + listState.canScrollBackward && !listState.canScrollForward + } + } + val partialCollapsedHeight = 64.dp + statusBarHeight val balanceBlockHeight = 320.dp + partialCollapsedHeight val behavior = rememberTangemExitUntilCollapsedScrollBehavior( expandedHeight = balanceBlockHeight, partialCollapsedHeight = partialCollapsedHeight, snapAnimationSpec = spring(stiffness = Spring.StiffnessMedium), + isTopOverscrollEnabled = isTopOverscrollEnabled, ) val coroutineScope = rememberCoroutineScope() @@ -113,6 +130,7 @@ internal fun WalletScreen2( bottomSheetContent = bottomSheetContent, bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, onBottomSheetStateChange = onBottomSheetStateChange, + listStates = listStates, ) WalletEventEffect( @@ -135,6 +153,7 @@ private fun WalletContent2( walletsPagerState: PagerState, tangemPayComponent: TangemPayMainBlockComponent, behavior: TangemCollapsingAppBarBehavior, + listStates: Map, bottomSheetHeaderHeightProvider: () -> Dp, onBottomSheetStateChange: (BottomSheetState) -> Unit, bottomSheetContent: @Composable (() -> Unit), @@ -174,12 +193,6 @@ private fun WalletContent2( } } - val listStates = rememberSaveable(saver = lazyListStateMapSaver(walletsPagerState.pageCount)) { - mutableMapOf().apply { - repeat(walletsPagerState.pageCount) { index -> put(index, LazyListState()) } - } - } - val canPagerScroll by remember { derivedStateOf { behavior.state.heightOffset == 0f } } val pullToRefreshState = rememberPullToRefreshState() @@ -220,7 +233,8 @@ private fun WalletContent2( LaunchedEffect(walletsPagerState.currentPage, currentWallet.walletsBalanceUM) { if (walletsPagerState.currentPage == currentWalletIndex) { - walletBalance = (currentWallet.walletsBalanceUM as? WalletBalanceUM.Content)?.balanceInAppBar + walletBalance = + (currentWallet.walletsBalanceUM as? WalletBalanceUM.Content)?.balanceInAppBar } } LaunchedEffect(walletsPagerState.currentPage, currentWallet.pullToRefreshConfig) { @@ -240,39 +254,45 @@ private fun WalletContent2( val pageSlideAlpha by rememberPageAlpha(walletsPagerState, currentWalletIndex) - TangemPullToRefreshSlidingContainer( - state = pullToRefreshState, - config = currentWallet.pullToRefreshConfig, - modifier = Modifier.alpha(pageSlideAlpha), - indicatorOffset = with(LocalDensity.current) { - behavior.state.partialHeightLimit.toDp() - }, + TangemSharedTransitionLayout( + modifier = Modifier + .fillMaxSize() + .alpha(pageSlideAlpha), ) { - TangemCollapsingTopBar( - state = behavior.state, - collapsingPart = { - WalletBalance( - behavior = behavior, - walletBalanceUM = currentWallet.walletsBalanceUM, - buttons = currentWallet.buttons, - isBalanceHidden = state.isHidingMode, - ) + TangemPullToRefreshSlidingContainer( + state = pullToRefreshState, + config = currentWallet.pullToRefreshConfig, + indicatorOffset = with(LocalDensity.current) { + behavior.state.partialHeightLimit.toDp() }, - body = { - WalletListContent( - currentWallet = currentWallet, - listState = listState, - isBalanceHidden = state.isHidingMode, - tangemPayComponent = tangemPayComponent, - contentPadding = contentPadding, - modifier = Modifier - .fillMaxSize() - .nestedScroll(behavior.nestedScrollConnection), - ) - }, - ) + ) { + TangemCollapsingTopBar( + state = behavior.state, + collapsingPart = { + WalletBalance( + behavior = behavior, + walletBalanceUM = currentWallet.walletsBalanceUM, + buttons = currentWallet.buttons, + isBalanceHidden = state.isHidingMode, + ) + }, + body = { + WalletListContent( + currentWallet = currentWallet, + listState = listState, + isBalanceHidden = state.isHidingMode, + contentPadding = contentPadding, + tangemPayComponent = tangemPayComponent, + modifier = Modifier + .fillMaxSize() + .nestedScroll(behavior.nestedScrollConnection), + ) + }, + ) + } - val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight + val peekHeight = + bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight MarketsHint( modifier = Modifier .align(Alignment.BottomCenter) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt index 5a17a286bc..5a64a3289b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt @@ -15,7 +15,6 @@ import com.tangem.common.ui.notifications.notificationsCarousel import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.txHistoryItems import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.TangemSharedTransitionLayout import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItems @@ -40,41 +39,40 @@ internal fun WalletListContent( val movableItemModifier = Modifier.padding(horizontal = TangemTheme.dimens2.x3) val itemModifier = movableItemModifier.padding(top = TangemTheme.dimens2.x3) - TangemSharedTransitionLayout(modifier) { - LazyColumn( - state = listState, - contentPadding = contentPadding, - horizontalAlignment = Alignment.CenterHorizontally, - overscrollEffect = rememberOverscrollEffect(), - ) { - notifications( - notifications = currentWallet.notifications.map { it.messageUM }.toPersistentList(), - contentColor = containerColor, - modifier = movableItemModifier, - ) - notificationsCarousel( - containerColor = containerColor, - modifier = movableItemModifier, - notifications = currentWallet.notificationsCarousel.map { it.messageUM }.toPersistentList(), - ) + LazyColumn( + modifier = modifier, + state = listState, + contentPadding = contentPadding, + horizontalAlignment = Alignment.CenterHorizontally, + overscrollEffect = rememberOverscrollEffect(), + ) { + notifications( + notifications = currentWallet.notifications.map { it.messageUM }.toPersistentList(), + contentColor = containerColor, + modifier = movableItemModifier, + ) + notificationsCarousel( + containerColor = containerColor, + modifier = movableItemModifier, + notifications = currentWallet.notificationsCarousel.map { it.messageUM }.toPersistentList(), + ) - tangemPay( - tangemPayComponent = tangemPayComponent, - tangemPayUM = currentWallet.tangemPayMainUM, - isBalanceHidden = isBalanceHidden, - modifier = itemModifier, - ) + tangemPay( + tangemPayComponent = tangemPayComponent, + tangemPayUM = currentWallet.tangemPayMainUM, + isBalanceHidden = isBalanceHidden, + modifier = itemModifier, + ) - tokensListItems2( - walletTokensListUM = currentWallet.tokensListUM, - modifier = movableItemModifier, - isBalanceHidden = isBalanceHidden, - ) + tokensListItems2( + walletTokensListUM = currentWallet.tokensListUM, + modifier = movableItemModifier, + isBalanceHidden = isBalanceHidden, + ) - nftCollections2(state = currentWallet, itemModifier = itemModifier) + nftCollections2(state = currentWallet, itemModifier = itemModifier) - organizeTokens2(state = currentWallet, itemModifier = itemModifier) - } + organizeTokens2(state = currentWallet, itemModifier = itemModifier) } } From 6a082c1d589a8c9b917543fd553d03b15a630c68 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Mar 2026 19:55:05 +0500 Subject: [PATCH 38/75] Updated on 2026-08-14 --- .../ui/ds/contextmenu/TangemContextMenu.kt | 54 ++++- .../core/ui/ds/row/token/TangemTokenRow.kt | 35 +-- .../core/ui/ds/row/token/TangemTokenRowUM.kt | 37 ++- .../internal/TangemTokenRowPreviewData.kt | 14 +- .../DefaultTokenActionsComponent.kt | 110 +++++++++ .../child/tokenActions/TokenActionContent.kt | 210 ++++++++++++++++++ .../tokenActions/TokenActionsComponent.kt | 66 +----- .../tokenActions/di/TokenActionsModule.kt | 16 ++ .../wallet/child/wallet/WalletComponent.kt | 17 +- .../intents/WalletContentClickIntents.kt | 51 +++++ .../common/preview/WalletScreenPreviewData.kt | 4 +- .../router/DefaultWalletRouter.kt | 12 +- .../presentation/router/InnerWalletRouter.kt | 9 +- .../wallet/state/model/TokenActionButtonUM.kt | 4 + .../wallet/state/model/WalletDialogConfig.kt | 4 + .../MultiWalletCurrencyActionsConverter.kt | 19 +- .../WalletTokenCurrencyItemConverter.kt | 9 +- .../presentation/wallet/ui/WalletScreen2.kt | 19 +- .../multicurrency/MultiCurrencyContent.kt | 31 ++- 19 files changed, 603 insertions(+), 118 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/DefaultTokenActionsComponent.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionContent.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/di/TokenActionsModule.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenu.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenu.kt index 69a475c2a8..3800173371 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenu.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenu.kt @@ -43,6 +43,7 @@ fun TangemContextMenu( modifier: Modifier = Modifier, offset: DpOffset = DpOffset.Zero, properties: PopupProperties = PopupProperties(focusable = true), + positionProvider: PopupPositionProvider? = null, content: @Composable ColumnScope.() -> Unit, ) { val expandedStates = remember { MutableTransitionState(false) } @@ -51,7 +52,7 @@ fun TangemContextMenu( if (expandedStates.currentState || expandedStates.targetState) { val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) } val density = LocalDensity.current - val popupPositionProvider = DropdownMenuPositionProvider( + val popupPositionProvider = positionProvider ?: DropdownMenuPositionProvider( offset, density, ) { parentBounds, menuBounds -> @@ -249,6 +250,57 @@ internal data class DropdownMenuPositionProvider( } } +/** + * A [PopupPositionProvider] that centers the popup horizontally on the screen + * and positions it below the anchor. If there is not enough space below, + * it positions the popup above the anchor. If there is no space in either direction, + * it reports the required vertical shift via [onAnchorShiftRequired] so the caller + * can move the anchor upward to make room below. + */ +@Immutable +class CenteredContextMenuPositionProvider( + private val contentOffset: DpOffset, + private val density: Density, + private val onAnchorShiftRequired: (Int) -> Unit = {}, +) : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + ): IntOffset { + val contentOffsetY = with(density) { contentOffset.y.roundToPx() } + val x = (windowSize.width - popupContentSize.width) / 2 + + val yBelow = anchorBounds.bottom + contentOffsetY + val yAbove = anchorBounds.top - contentOffsetY - popupContentSize.height + + val isFitsBelow = yBelow + popupContentSize.height <= windowSize.height + val isFitsAbove = yAbove >= 0 + + val y = when { + isFitsBelow -> { + onAnchorShiftRequired(0) + yBelow + } + isFitsAbove -> { + onAnchorShiftRequired(0) + yAbove + } + else -> { + // Neither fits — calculate how much the anchor must shift up + // so the popup fits below. Place popup at bottom edge of screen. + val desiredY = windowSize.height - popupContentSize.height + val shift = yBelow - desiredY + onAnchorShiftRequired(shift) + desiredY + } + } + + return IntOffset(x, y) + } +} + // region Preview @Composable @Preview(showBackground = true, widthDp = 360) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt index 307dafb258..67d8918c7f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt @@ -1,18 +1,13 @@ package com.tangem.core.ui.ds.row.token import android.content.res.Configuration -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.layoutId -import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -110,7 +105,7 @@ fun TangemTokenRow( .fillMaxWidth(), ) }, - modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM), + modifier = modifier, ) } @@ -198,36 +193,10 @@ fun TangemTokenRow( .testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK), ) }, - modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM), + modifier = modifier, ) } -@OptIn(ExperimentalFoundationApi::class) -private fun Modifier.tokenClickable(tokenRowUM: TangemTokenRowUM): Modifier = composed { - val hapticFeedback = LocalHapticFeedback.current - - val onClick = tokenRowUM.onItemClick - val onLongClick = tokenRowUM.onItemLongClick - val onHapticLongClick = if (onLongClick != null) { - { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - onLongClick() - } - } else { - null - } - - when { - onClick == null && onLongClick == null -> this - onClick == null && onLongClick != null -> combinedClickable(onClick = {}, onLongClick = onHapticLongClick) - onClick != null && onLongClick == null -> combinedClickable(onClick = onClick) - onClick != null && onLongClick != null -> { - combinedClickable(onClick = onClick, onLongClick = onHapticLongClick) - } - else -> this - } -} - // region Preview @Composable @Preview(showBackground = true, widthDp = 360) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt index 814e4dbfa3..b246291079 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.ds.row.token import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable +import androidx.compose.ui.geometry.Offset import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.ds.badge.TangemBadgeUM @@ -11,7 +12,9 @@ import com.tangem.core.ui.ds.row.internal.TangemRowTailUM import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.serialization.Serializable +@Serializable @Immutable sealed class TangemTokenRowUM : TangemRowUM { @@ -43,11 +46,12 @@ sealed class TangemTokenRowUM : TangemRowUM { abstract val onItemClick: (() -> Unit)? /** Callback which will be called when an item is long clicked */ - abstract val onItemLongClick: (() -> Unit)? + abstract val onItemLongClick: ((Offset, TangemTokenRowUM) -> Any)? /** * Content state of [TangemTokenRowUM] */ + @Serializable data class Content( override val id: String, override val headIconUM: TangemIconUM.Currency, @@ -58,12 +62,13 @@ sealed class TangemTokenRowUM : TangemRowUM { override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty, override val tailUM: TangemRowTailUM = TangemRowTailUM.Empty, override val onItemClick: (() -> Unit)?, - override val onItemLongClick: (() -> Unit)?, + override val onItemLongClick: ((Offset, TangemTokenRowUM) -> Any)?, ) : TangemTokenRowUM() /** * Loading state of [TangemTokenRowUM] */ + @Serializable data class Loading( override val id: String, override val headIconUM: TangemIconUM.Currency = TangemIconUM.Currency(CurrencyIconState.Loading), @@ -75,12 +80,13 @@ sealed class TangemTokenRowUM : TangemRowUM { override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty override val tailUM: TangemRowTailUM = TangemRowTailUM.Empty override val onItemClick: (() -> Unit)? = null - override val onItemLongClick: (() -> Unit)? = null + override val onItemLongClick: ((Offset, TangemTokenRowUM) -> Unit)? = null } /** * Loading state of [TangemTokenRowUM] */ + @Serializable data class Empty( override val id: String, ) : TangemTokenRowUM() { @@ -92,12 +98,13 @@ sealed class TangemTokenRowUM : TangemRowUM { override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty override val tailUM: TangemRowTailUM = TangemRowTailUM.Empty override val onItemClick: (() -> Unit)? = null - override val onItemLongClick: (() -> Unit)? = null + override val onItemLongClick: ((Offset, TangemTokenRowUM) -> Unit)? = null } /** * Actionable state of [TangemTokenRowUM] */ + @Serializable data class Actionable( override val id: String, override val headIconUM: TangemIconUM.Currency, @@ -105,16 +112,17 @@ sealed class TangemTokenRowUM : TangemRowUM { override val subtitleUM: SubtitleUM, override val tailUM: TangemRowTailUM, override val onItemClick: (() -> Unit)?, - override val onItemLongClick: (() -> Unit)?, + override val onItemLongClick: ((Offset, TangemTokenRowUM) -> Unit)?, override val topEndContentUM: EndContentUM = EndContentUM.Empty, override val bottomEndContentUM: EndContentUM = EndContentUM.Empty, ) : TangemTokenRowUM() { override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty } + @Serializable @Immutable sealed class TitleUM { - + @Serializable data class Content( val text: TextReference, val hasPending: Boolean = false, @@ -123,16 +131,20 @@ sealed class TangemTokenRowUM : TangemRowUM { val onBadgeClick: (() -> Unit)? = null, ) : TitleUM() + @Serializable data object Loading : TitleUM() + @Serializable data object Placeholder : TitleUM() + @Serializable data object Empty : TitleUM() } + @Serializable @Immutable sealed class SubtitleUM { - + @Serializable data class Content( val text: TextReference, val isAvailable: Boolean = true, @@ -142,16 +154,20 @@ sealed class TangemTokenRowUM : TangemRowUM { val badge: TangemBadgeUM? = null, ) : SubtitleUM() + @Serializable data object Loading : SubtitleUM() + @Serializable data object Placeholder : SubtitleUM() + @Serializable data object Empty : SubtitleUM() } + @Serializable @Immutable sealed class EndContentUM { - + @Serializable data class Content( val text: TextReference, val isAvailable: Boolean = true, @@ -161,13 +177,17 @@ sealed class TangemTokenRowUM : TangemRowUM { val priceChangeUM: PriceChangeState = PriceChangeState.Unknown, ) : EndContentUM() + @Serializable data object Loading : EndContentUM() + @Serializable data object Placeholder : EndContentUM() + @Serializable data object Empty : EndContentUM() } + @Serializable @Immutable sealed class PromoBannerUM { data class Content( @@ -184,6 +204,7 @@ sealed class TangemTokenRowUM : TangemRowUM { } } + @Serializable data object Empty : PromoBannerUM() } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt index bd003a5ee9..93e7e86184 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt @@ -128,7 +128,7 @@ object TangemTokenRowPreviewData { promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty, tailUM = TangemRowTailUM.Empty, onItemClick = {}, - onItemLongClick = {}, + onItemLongClick = { _, _ -> }, ) val defaultEllipsisState: TangemTokenRowUM.Content @@ -155,7 +155,7 @@ object TangemTokenRowPreviewData { promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty, tailUM = TangemRowTailUM.Empty, onItemClick = {}, - onItemLongClick = {}, + onItemLongClick = { _, _ -> }, ) val tokenState: TangemTokenRowUM.Content @@ -169,7 +169,7 @@ object TangemTokenRowPreviewData { promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty, tailUM = TangemRowTailUM.Empty, onItemClick = {}, - onItemLongClick = {}, + onItemLongClick = { _, _ -> }, ) val customTokenState: TangemTokenRowUM.Content @@ -183,7 +183,7 @@ object TangemTokenRowPreviewData { promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty, tailUM = TangemRowTailUM.Empty, onItemClick = {}, - onItemLongClick = {}, + onItemLongClick = { _, _ -> }, ) val draggableState: TangemTokenRowUM.Actionable @@ -194,7 +194,7 @@ object TangemTokenRowPreviewData { subtitleUM = subtitleUM, tailUM = TangemRowTailUM.Draggable(R.drawable.ic_drag_24), onItemClick = {}, - onItemLongClick = {}, + onItemLongClick = { _, _ -> }, ) val draggableStateV2: TangemTokenRowUM.Actionable @@ -207,7 +207,7 @@ object TangemTokenRowPreviewData { bottomEndContentUM = bottomEndContentUM, tailUM = TangemRowTailUM.Draggable(R.drawable.ic_drag_24), onItemClick = {}, - onItemLongClick = {}, + onItemLongClick = { _, _ -> }, ) val loadingState: TangemTokenRowUM.Loading @@ -252,7 +252,7 @@ object TangemTokenRowPreviewData { priceChangeUM = priceChangeState, ), onItemClick = {}, - onItemLongClick = {}, + onItemLongClick = { _, _ -> }, ) val accountLetterState: TangemTokenRowUM.Content diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/DefaultTokenActionsComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/DefaultTokenActionsComponent.kt new file mode 100644 index 0000000000..12dede4099 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/DefaultTokenActionsComponent.kt @@ -0,0 +1,110 @@ +package com.tangem.feature.wallet.child.tokenActions + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.DpOffset +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.components.SimpleSettingsRow +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.components.getDefaultRowColors +import com.tangem.core.ui.components.getWarningRowColors +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.feature.wallet.child.tokenActions.TokenActionsComponent.Params +import com.tangem.feature.wallet.presentation.wallet.ui.components.fastForEach +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.* + +internal class DefaultTokenActionsComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: Params, + val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, +) : TokenActionsComponent, AppComponentContext by appComponentContext { + + val isBalanceHiddenFlow: StateFlow + field = MutableStateFlow(false) + + init { + getBalanceHidingSettingsUseCase() + .conflate() + .distinctUntilChanged() + .onEach { + isBalanceHiddenFlow.value = it.isBalanceHidden + } + .launchIn(componentScope) + } + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + if (!LocalRedesignEnabled.current) { + TangemBottomSheet( + containerColor = TangemTheme.colors.background.primary, + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ), + ) { + Column { + params.actions.fastForEach { action -> + if (action.isEnabled) { + val rowColors = if (action.isWarning) { + getWarningRowColors() + } else { + getDefaultRowColors() + } + SimpleSettingsRow( + title = action.text.resolveReference(), + icon = action.iconResId, + enabled = action.isEnabled, + rowColors = rowColors, + onItemsClick = action.onClick, + ) + } + } + } + } + } + } + + @Composable + override fun Content(modifier: Modifier) { + if (params.tokenRowUM == null) { + dismiss() + } else { + val isBalanceHidden by isBalanceHiddenFlow.collectAsStateWithLifecycle() + if (LocalRedesignEnabled.current) { + val offset = with(LocalDensity.current) { + DpOffset(params.offsetX.toDp(), params.offsetY.toDp()) + } + TokenActionContent( + tokenRowUM = params.tokenRowUM, + isBalanceHidden = isBalanceHidden, + offset = offset, + actions = params.actions, + onDismiss = params.onDismiss, + modifier = modifier, + ) + } + } + } + + @AssistedFactory + interface Factory : TokenActionsComponent.Factory { + override fun create(context: AppComponentContext, params: Params): DefaultTokenActionsComponent + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionContent.kt new file mode 100644 index 0000000000..aebf8404a9 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionContent.kt @@ -0,0 +1,210 @@ +package com.tangem.feature.wallet.child.tokenActions + +import android.content.res.Configuration +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.ds.contextmenu.CenteredContextMenuPositionProvider +import com.tangem.core.ui.ds.contextmenu.TangemContextMenu +import com.tangem.core.ui.ds.row.token.TangemTokenRow +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.ds.row.token.internal.TangemTokenRowPreviewData +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun TokenActionContent( + tokenRowUM: TangemTokenRowUM, + isBalanceHidden: Boolean, + offset: DpOffset, + actions: ImmutableList, + modifier: Modifier = Modifier, + onDismiss: () -> Unit, +) { + val density = LocalDensity.current + var anchorShiftPx by remember { mutableIntStateOf(0) } + val anchorShiftDp = with(density) { anchorShiftPx.toDp() } + val animatedShift by animateDpAsState( + targetValue = anchorShiftDp, + animationSpec = tween(), + label = "AnchorShift", + ) + + Box(modifier.fillMaxSize()) { + Box(modifier = Modifier.offset(y = offset.y - animatedShift)) { + TangemTokenRow( + tokenRowUM = tokenRowUM, + isBalanceHidden = isBalanceHidden, + reorderableState = null, + modifier = Modifier + .padding(horizontal = TangemTheme.dimens2.x3) + .clip(RoundedCornerShape(18.dp)) + .background(TangemTheme.colors2.surface.level3), + ) + TangemContextMenu( + expanded = true, + onDismissRequest = onDismiss, + positionProvider = remember(density) { + CenteredContextMenuPositionProvider( + contentOffset = DpOffset(x = 0.dp, y = 12.dp), + density = density, + onAnchorShiftRequired = { shift -> + if (anchorShiftPx == 0 && shift > 0) { + anchorShiftPx = shift + } + }, + ) + }, + ) { + TokenActionContextMenuContent( + actions = actions, + onDismiss = onDismiss, + ) + } + } + } +} + +@Composable +private fun TokenActionContextMenuContent(actions: ImmutableList, onDismiss: () -> Unit) { + Column( + modifier = Modifier + .widthIn(min = 206.dp) + .padding( + vertical = TangemTheme.dimens2.x2_5, + horizontal = TangemTheme.dimens2.x4, + ), + ) { + actions.fastForEach { item -> + Column { + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + modifier = Modifier + .clickable( + enabled = item.isEnabled, + onClick = { + item.onClick() + onDismiss() + }, + ) + .padding( + start = TangemTheme.dimens2.x1_5, + end = TangemTheme.dimens2.x2, + top = TangemTheme.dimens2.x2_5, + bottom = TangemTheme.dimens2.x2_5, + ), + ) { + Icon( + imageVector = ImageVector.vectorResource(item.iconResId), + contentDescription = null, + tint = if (item.isWarning) { + TangemTheme.colors2.graphic.status.warning + } else { + TangemTheme.colors2.graphic.neutral.primary + }, + modifier = Modifier.size(TangemTheme.dimens2.x5), + ) + Text( + text = item.text.resolveReference(), + style = TangemTheme.typography2.headingRegular17, + color = if (item.isWarning) { + TangemTheme.colors2.text.status.warning + } else { + TangemTheme.colors2.text.neutral.primary + }, + ) + } + if (item.hasDivider) { + Spacer( + modifier = Modifier + .padding( + vertical = TangemTheme.dimens2.x2_5, + horizontal = TangemTheme.dimens2.x2, + ) + .fillMaxWidth() + .height(1.dp) + .background(TangemTheme.colors2.border.neutral.primary), + ) + } + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TokenActionContent_Preview() { + TangemThemePreviewRedesign { + TokenActionContent( + tokenRowUM = TangemTokenRowPreviewData.tokenState, + offset = DpOffset( + x = 100.dp, + y = 100.dp, + ), + actions = persistentListOf( + TokenActionButtonUM( + id = "Send", + text = stringReference("Send"), + iconResId = R.drawable.ic_arrow_up_24, + isEnabled = true, + isWarning = false, + hasDivider = false, + onClick = {}, + ), + TokenActionButtonUM( + id = "Receive", + text = stringReference("Receive"), + iconResId = R.drawable.ic_arrow_down_24, + isEnabled = true, + isWarning = false, + hasDivider = false, + onClick = {}, + ), + TokenActionButtonUM( + id = "Swap", + text = stringReference("Swap"), + iconResId = R.drawable.ic_exchange_vertical_24, + isEnabled = true, + isWarning = false, + hasDivider = true, + onClick = {}, + ), + TokenActionButtonUM( + id = "Remove", + text = stringReference("Remove"), + iconResId = R.drawable.ic_trash_24, + isEnabled = true, + isWarning = true, + hasDivider = false, + onClick = {}, + ), + ), + isBalanceHidden = false, + onDismiss = {}, + ) + } +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionsComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionsComponent.kt index aa0ae8e9d7..106516a350 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionsComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionsComponent.kt @@ -1,64 +1,20 @@ package com.tangem.feature.wallet.child.tokenActions -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.ui.components.SimpleSettingsRow -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.getDefaultRowColors -import com.tangem.core.ui.components.getWarningRowColors +import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonUM -import com.tangem.feature.wallet.presentation.wallet.ui.components.fastForEach -import dagger.assisted.Assisted -import dagger.assisted.AssistedInject - -internal class TokenActionsComponent @AssistedInject constructor( - @Assisted appComponentContext: AppComponentContext, - @Assisted private val params: Params, -) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext { - - override fun dismiss() { - params.onDismiss() - } - - @Composable - override fun BottomSheet() { - TangemBottomSheet( - containerColor = TangemTheme.colors.background.primary, - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = ::dismiss, - content = TangemBottomSheetConfigContent.Empty, - ), - ) { - Column { - params.actions.fastForEach { action -> - if (action.isEnabled) { - val rowColors = if (action.isWarning) { - getWarningRowColors() - } else { - getDefaultRowColors() - } - SimpleSettingsRow( - title = action.text.resolveReference(), - icon = action.iconResId, - enabled = action.isEnabled, - rowColors = rowColors, - onItemsClick = action.onClick, - ) - } - } - } - } - } +import kotlinx.collections.immutable.ImmutableList +internal interface TokenActionsComponent : ComposableBottomSheetComponent, ComposableContentComponent { data class Params( - val actions: List, + val actions: ImmutableList, + val tokenRowUM: TangemTokenRowUM?, + val offsetX: Float, + val offsetY: Float, val onDismiss: () -> Unit, ) + + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/di/TokenActionsModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/di/TokenActionsModule.kt new file mode 100644 index 0000000000..61733c285a --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/di/TokenActionsModule.kt @@ -0,0 +1,16 @@ +package com.tangem.feature.wallet.child.tokenActions.di + +import com.tangem.feature.wallet.child.tokenActions.DefaultTokenActionsComponent +import com.tangem.feature.wallet.child.tokenActions.TokenActionsComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal interface TokenActionsModule { + + @Binds + fun bindTokenActionsComponentFactory(impl: DefaultTokenActionsComponent.Factory): TokenActionsComponent.Factory +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index a40b3e413a..f15fbdfb75 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -17,12 +17,14 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.DesignFeatureToggles import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.components.haze.hazeEffectTangem import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableDialogComponent import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.tokens.model.details.TokenAction import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent +import com.tangem.feature.wallet.child.tokenActions.DefaultTokenActionsComponent import com.tangem.feature.wallet.child.tokenActions.TokenActionsComponent import com.tangem.feature.wallet.child.wallet.model.WalletModel import com.tangem.feature.wallet.navigation.WalletRoute @@ -61,6 +63,7 @@ internal class WalletComponent @AssistedInject constructor( private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory, private val newPromoBannersFeatureToggles: NewPromoBannersFeatureToggles, private val networkSelectionComponentFactory: NetworkSelectionComponent.Factory, + private val tokenActionsComponentFactory: TokenActionsComponent.Factory, private val designFeatureToggles: DesignFeatureToggles, ) : ComposableContentComponent, AppComponentContext by appComponentContext { @@ -164,11 +167,14 @@ internal class WalletComponent @AssistedInject constructor( ) } is WalletDialogConfig.TokenActionList -> { - TokenActionsComponent( - appComponentContext = childByContext(componentContext), + tokenActionsComponentFactory.create( + context = childByContext(componentContext), params = TokenActionsComponent.Params( actions = dialogConfig.actionList, onDismiss = model.innerWalletRouter.dialogNavigation::dismiss, + tokenRowUM = dialogConfig.tokenRowUM, + offsetX = dialogConfig.offsetX, + offsetY = dialogConfig.offsetY, ), ) } @@ -261,6 +267,13 @@ internal class WalletComponent @AssistedInject constructor( when (val dialog = dialog.child?.instance) { is ComposableDialogComponent -> dialog.Dialog() + is DefaultTokenActionsComponent -> { + if (designFeatureToggles.isRedesignEnabled) { + dialog.Content(Modifier.hazeEffectTangem()) + } else { + dialog.BottomSheet() + } + } is ComposableBottomSheetComponent -> dialog.BottomSheet() else -> {} } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index 92136614c6..8158d28f2b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.child.wallet.model.intents +import androidx.compose.ui.geometry.Offset import arrow.core.getOrElse import com.tangem.utils.logging.TangemLogger import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig @@ -9,6 +10,7 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId @@ -38,6 +40,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBott import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCurrencyActionsConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch @@ -56,6 +59,13 @@ internal interface WalletContentClickIntents { fun onTokenItemLongClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onTokenItemLongClickV2( + accountId: AccountId, + cryptoCurrencyStatus: CryptoCurrencyStatus, + offset: Offset, + tokenRowUM: TangemTokenRowUM, + ) + fun onApyLabelClick(accountId: AccountId, currencyStatus: CryptoCurrencyStatus, apySource: ApySource, apy: String) fun onYieldPromoCloseClick() @@ -153,6 +163,47 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( accountId = accountId, clickIntents = currencyActionsClickIntents, ).convert(actionsState), + offset = Offset.Zero, + tokenRowUM = null, + ) + } + } + } + + override fun onTokenItemLongClickV2( + accountId: AccountId, + cryptoCurrencyStatus: CryptoCurrencyStatus, + offset: Offset, + tokenRowUM: TangemTokenRowUM, + ) { + modelScope.launch { + val userWalletId = accountId.userWalletId + val userWallet = getUserWalletUseCase(userWalletId).getOrElse { exception -> + TangemLogger.e( + """ + Unable to get user wallet + |- ID: $userWalletId + |- Exception: $exception + """.trimIndent(), + ) + + return@launch + } + + getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus) + .take(count = 1) + .collectLatest { actionsState -> + router.openTokenActionSheet( + userWallet = userWallet, + tokenActionList = MultiWalletCurrencyActionsConverter( + userWallet = userWallet, + accountId = accountId, + clickIntents = currencyActionsClickIntents, + ).convert(actionsState) + .filter { it.isEnabled } + .toPersistentList(), + offset = offset, + tokenRowUM = tokenRowUM, ) } } 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 44b3aa8329..5668d743d8 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 @@ -40,7 +40,7 @@ internal object WalletScreenPreviewData { promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty, tailUM = TangemRowTailUM.Empty, onItemClick = {}, - onItemLongClick = {}, + onItemLongClick = { _, _ -> }, ) private val accountRowDefault = TangemTokenRowUM.Content( @@ -61,7 +61,7 @@ internal object WalletScreenPreviewData { promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty, tailUM = TangemRowTailUM.Empty, onItemClick = {}, - onItemLongClick = {}, + onItemLongClick = { _, _ -> }, ) private val tokenListDefault = WalletTokensListUM.Content( 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 95c94d246c..1eadd2b07b 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 @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.router +import androidx.compose.ui.geometry.Offset import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss @@ -8,6 +9,7 @@ import com.tangem.common.routing.AppRouter import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.DesignFeatureToggles +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency @@ -161,10 +163,18 @@ internal class DefaultWalletRouter @Inject constructor( ) } - override fun openTokenActionSheet(userWallet: UserWallet, tokenActionList: ImmutableList) { + override fun openTokenActionSheet( + userWallet: UserWallet, + tokenActionList: ImmutableList, + offset: Offset, + tokenRowUM: TangemTokenRowUM?, + ) { dialogNavigation.activate( configuration = WalletDialogConfig.TokenActionList( actionList = tokenActionList, + offsetX = offset.x, + offsetY = offset.y, + tokenRowUM = tokenRowUM, ), ) } 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 fb55f96999..7d772e539c 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 @@ -1,8 +1,10 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Stable +import androidx.compose.ui.geometry.Offset import com.arkivanov.decompose.router.slot.SlotNavigation import com.tangem.common.routing.AppRoute +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency @@ -86,7 +88,12 @@ internal interface InnerWalletRouter { fun openYieldSupplyEntryScreen(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, apy: String) /** Open token action sheet */ - fun openTokenActionSheet(userWallet: UserWallet, tokenActionList: ImmutableList) + fun openTokenActionSheet( + userWallet: UserWallet, + tokenActionList: ImmutableList, + offset: Offset, + tokenRowUM: TangemTokenRowUM?, + ) /** Open QR scanner screen */ fun openQrScanner() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenActionButtonUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenActionButtonUM.kt index 1b678f372d..46f75be772 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenActionButtonUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenActionButtonUM.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.annotation.DrawableRes +import androidx.compose.runtime.Stable import com.tangem.core.ui.extensions.TextReference import kotlinx.serialization.Serializable @@ -13,11 +14,14 @@ import kotlinx.serialization.Serializable * @property isWarning if warning row * @property isEnabled enabled */ +@Stable @Serializable data class TokenActionButtonUM( + val id: String, val text: TextReference, @DrawableRes val iconResId: Int, val onClick: () -> Unit, val isWarning: Boolean, val isEnabled: Boolean = true, + val hasDivider: Boolean = false, ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt index a8844378c1..8322d1c8df 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.model +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName @@ -34,6 +35,9 @@ internal sealed interface WalletDialogConfig { @Serializable data class TokenActionList( val actionList: ImmutableList, + val tokenRowUM: TangemTokenRowUM?, + val offsetY: Float, + val offsetX: Float, ) : WalletDialogConfig @Serializable diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index 4044cedde1..95105edff0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -23,11 +23,25 @@ internal class MultiWalletCurrencyActionsConverter( ) : Converter> { override fun convert(value: TokenActionsState): ImmutableList { - return value.states - .filterIfSingleWithToken() + val actionList = value.states.filterIfSingleWithToken() .mapNotNull { mapTokenActionState(actionsState = it, cryptoCurrencyStatus = value.cryptoCurrencyStatus) } + + return actionList + .mapIndexed { index, action -> + val analyticsAction = TokenActionsState.ActionState.Analytics::class.java.simpleName + val hideTokenAction = TokenActionsState.ActionState.HideToken::class.java.simpleName + + if ( + action.id == analyticsAction || + index != actionList.lastIndex && actionList[index + 1].id == hideTokenAction + ) { + action.copy(hasDivider = true) + } else { + action + } + } .toImmutableList() } @@ -111,6 +125,7 @@ internal class MultiWalletCurrencyActionsConverter( } return TokenActionButtonUM( + id = actionsState::class.java.simpleName, text = title, iconResId = icon, onClick = action, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenCurrencyItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenCurrencyItemConverter.kt index c1794a9422..8697f5a9eb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenCurrencyItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenCurrencyItemConverter.kt @@ -76,8 +76,13 @@ internal class WalletTokenCurrencyItemConverter( onItemLongClick = when (value.value) { CryptoCurrencyStatus.Loading -> null else -> { - { - clickIntents.onTokenItemLongClick(accountId, value) + { offset, tokenRowUM -> + clickIntents.onTokenItemLongClickV2( + accountId = accountId, + cryptoCurrencyStatus = value, + offset = offset, + tokenRowUM = tokenRowUM, + ) } } }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt index a8c9e5a8c0..376948e5ae 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt @@ -50,6 +50,7 @@ import com.tangem.core.ui.components.background.northernlights.NorthernLightsBac import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetDraggableHeader import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshSlidingContainer +import com.tangem.core.ui.components.haze.hazeEffectTangem import com.tangem.core.ui.components.haze.hazeSourceTangem import com.tangem.core.ui.components.rememberIsKeyboardVisible import com.tangem.core.ui.components.sheetscaffold.* @@ -72,6 +73,8 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.common.Wallet import com.tangem.feature.wallet.presentation.wallet.ui.utils.lazyListStateMapSaver import com.tangem.features.tangempay.component.TangemPayMainBlockComponent import com.tangem.features.tangempay.entity.TangemPayMainUM +import dev.chrisbanes.haze.HazeProgressive +import dev.chrisbanes.haze.HazeTint import kotlinx.coroutines.launch import kotlin.math.abs @@ -82,6 +85,7 @@ private const val MARKET_HINT_THRESHOLD = 0.5f internal fun WalletScreen2( state: WalletScreenState, tangemPayComponent: TangemPayMainBlockComponent, + modifier: Modifier = Modifier, bottomSheetContent: @Composable (() -> Unit), bottomSheetHeaderHeightProvider: () -> Dp, onBottomSheetStateChange: (BottomSheetState) -> Unit, @@ -130,6 +134,7 @@ internal fun WalletScreen2( bottomSheetContent = bottomSheetContent, bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, onBottomSheetStateChange = onBottomSheetStateChange, + modifier = modifier, listStates = listStates, ) @@ -154,6 +159,7 @@ private fun WalletContent2( tangemPayComponent: TangemPayMainBlockComponent, behavior: TangemCollapsingAppBarBehavior, listStates: Map, + modifier: Modifier = Modifier, bottomSheetHeaderHeightProvider: () -> Dp, onBottomSheetStateChange: (BottomSheetState) -> Unit, bottomSheetContent: @Composable (() -> Unit), @@ -169,6 +175,7 @@ private fun WalletContent2( } BaseScaffoldWithMarkets( + modifier = modifier, state = state, bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, onBottomSheetStateChange = onBottomSheetStateChange, @@ -200,7 +207,7 @@ private fun WalletContent2( Box( modifier = Modifier .fillMaxSize() - .hazeSourceTangem(zIndex = -1f), + .hazeSourceTangem(zIndex = -2f), ) { NorthernLightsBackground( containerColor = if (LocalIsInDarkTheme.current) { @@ -220,10 +227,20 @@ private fun WalletContent2( behavior = behavior, ) + val overlay = TangemTheme.colors2.overlay.overlayPrimary + HorizontalPager( state = walletsPagerState, userScrollEnabled = canPagerScroll, beyondViewportPageCount = 1, + modifier = Modifier.hazeEffectTangem { + fallbackTint = HazeTint(color = overlay) + progressive = HazeProgressive.verticalGradient( + startIntensity = 1f, + endIntensity = 1f, + preferPerformance = true, + ) + }, ) { currentWalletIndex -> val listState = listStates[currentWalletIndex] ?: rememberLazyListState() 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 26556fb7a8..a04c4bbd1e 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 @@ -6,6 +6,7 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateIntAsState import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -17,8 +18,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.layout.positionOnScreen import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.semantics @@ -155,12 +160,19 @@ private fun LazyListScope.tokenItem( backgroundColor = TangemTheme.colors2.surface.level3, ) + var position by remember { mutableStateOf(Offset.Zero) } when (val tokenRowUM = listItem.tokenRowUM) { is TangemTokenRowUM -> TangemTokenRow( tokenRowUM = tokenRowUM, isBalanceHidden = isBalanceHidden, reorderableState = null, - modifier = itemModifier, + modifier = itemModifier + .onGloballyPositioned { position = it.positionOnScreen() } + .combinedClickable( + enabled = tokenRowUM.onItemClick != null || tokenRowUM.onItemLongClick != null, + onClick = tokenRowUM.onItemClick ?: {}, + onLongClick = { tokenRowUM.onItemLongClick?.invoke(position, tokenRowUM) }, + ), ) is TangemHeaderRowUM -> TangemHeaderRow( headerRowUM = tokenRowUM, @@ -170,6 +182,7 @@ private fun LazyListScope.tokenItem( } } +@Suppress("LongMethod") private fun LazyListScope.portfolioItem( listItem: TokensListItemUM2.Portfolio, index: Int, @@ -220,12 +233,23 @@ private fun LazyListScope.portfolioItem( .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) .semantics { lazyListItemPosition = tokenIndex + 1 } + var position by remember { mutableStateOf(Offset.Zero) } when (val tokenRowUM = item.tokenRowUM) { is TangemTokenRowUM -> TangemTokenRow( tokenRowUM = tokenRowUM, isBalanceHidden = isBalanceHidden, reorderableState = null, - modifier = itemModifier, + modifier = itemModifier + .onGloballyPositioned { + position = it.positionInWindow() + } + .combinedClickable( + enabled = tokenRowUM.onItemClick != null || tokenRowUM.onItemLongClick != null, + onClick = tokenRowUM.onItemClick ?: {}, + onLongClick = { + tokenRowUM.onItemLongClick?.invoke(position, tokenRowUM) + }, + ), ) is TangemHeaderRowUM -> TangemHeaderRow( headerRowUM = tokenRowUM, @@ -343,7 +367,8 @@ internal fun PortfolioRowItem( val composables = remember { SharedTokenRowComposables( icon = { modifier -> - val size = if (isExpandedWrapped) AccountIconSize.ExtraSmall else AccountIconSize.Default + val size = + if (isExpandedWrapped) AccountIconSize.ExtraSmall else AccountIconSize.Default val headIcon = item.tokenRowUM.headIconUM val sizedHeadIcon = if (headIcon is TangemIconUM.Currency) { headIcon.copy( From 07304621a929d70d9c12be1d4adeb53ada621b65 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Mar 2026 18:04:10 +0300 Subject: [PATCH 39/75] Updated on 2026-08-14 --- .../main/java/com/tangem/core/ui/components/Fade.kt | 9 +++++++++ .../com/tangem/core/ui/components/haze/HazeExt.kt | 11 ++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt index d9fd87ea4b..f6e54c8b3b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt @@ -3,6 +3,8 @@ package com.tangem.core.ui.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color @@ -10,6 +12,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.res.LocalPowerSavingState import com.tangem.core.ui.res.TangemTheme import dev.chrisbanes.haze.HazeProgressive import dev.chrisbanes.haze.HazeStyle @@ -61,6 +64,12 @@ fun BottomFade(gradientBrush: Brush, modifier: Modifier = Modifier) { @Composable fun BottomFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) { val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + val isPowerSavingState by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState() + + if (isPowerSavingState) { + BottomFade(backgroundColor = backgroundColor, modifier = modifier) + return + } Box( modifier = modifier diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt b/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt index dd40c612c7..692ece38c2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt @@ -42,29 +42,30 @@ fun Modifier.hazeEffectTangem( return hazeEffect(state, style) { fallbackTint = HazeTint(rootBackground.copy(alpha = 0.5f)) - if (isGlobalBlurEnabled) { - configure() - } + configure() blurEnabled = blurEnabled && isGlobalBlurEnabled } } /** - * Applies a haze foreground effect to the [Modifier] with consideration of power saving mode. + * Applies a haze foreground effect to the [Modifier] * * @param style The [HazeStyle] to apply. Defaults to [HazeStyle.Unspecified]. * @param isBlurEnabled A Boolean indicating whether blur is enabled. Defaults to true. + * @param reactToPowerSavingMode A Boolean indicating whether the haze effect should react to power saving mode. + * Defaults to false. * @param configure A lambda to configure the [HazeEffectScope]. * @return A [Modifier] with the configured haze foreground effect applied. */ @Composable fun Modifier.hazeForegroundEffectTangem( style: HazeStyle = HazeStyle.Unspecified, + reactToPowerSavingMode: Boolean = false, isBlurEnabled: Boolean = true, configure: HazeEffectScope.() -> Unit = {}, ): Modifier { val powerSavingEnabled = LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState() - val isGlobalBlurEnabled = isBlurEnabled && !powerSavingEnabled.value + val isGlobalBlurEnabled = isBlurEnabled && (!reactToPowerSavingMode || !powerSavingEnabled.value) return hazeEffect( style = style, From 4afbb72f6dd36d703f29c971b5ebf4dc777cab21 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Mar 2026 14:08:26 +0300 Subject: [PATCH 40/75] Updated on 2026-08-14 --- .../permission/state/GiveTxPermissionState.kt | 1 + .../approval/api/GiveApprovalComponent.kt | 1 + .../impl/DefaultGiveApprovalComponent.kt | 1 + .../approval/impl/model/GiveApprovalModel.kt | 1 + .../approval/impl/model/GiveApprovalUM.kt | 1 + .../approval/impl/ui/GiveApprovalContent.kt | 34 ++++++++++++------ .../impl/presentation/model/StakingModel.kt | 1 + .../SetButtonsStateTransformer.kt | 35 ++++++++++--------- 8 files changed, 48 insertions(+), 27 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt index bcf8f4064e..baa901aeba 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt @@ -42,6 +42,7 @@ enum class ApproveType(val text: TextReference) { data class ApprovePermissionButton( val isEnabled: Boolean, val isLoading: Boolean = false, + val isHoldToConfirm: Boolean = false, val onClick: () -> Unit, ) diff --git a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt index e2345bbc2d..10506d2ba5 100644 --- a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt +++ b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt @@ -15,6 +15,7 @@ interface GiveApprovalComponent : ComposableBottomSheetComponent { val amount: String, val spenderAddress: String, val subtitle: TextReference, + val isHoldToConfirm: Boolean = false, val callback: Callback, ) diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt index c0102d54a6..756d19c2be 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt @@ -86,6 +86,7 @@ internal class DefaultGiveApprovalComponent @AssistedInject constructor( walletInteractionIcon = uiState.walletInteractionIcon, isApproveEnabled = uiState.isApproveButtonEnabled, isApproveLoading = uiState.isApproveLoading, + isHoldToConfirm = uiState.isHoldToConfirm, onApproveClick = model::onApproveClick, onCancelClick = model::onCancelClick, onOpenLearnMoreAboutApproveClick = model::onOpenLearnMoreAboutApproveClick, diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt index 3d328cc6bf..9873b3d65a 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt @@ -76,6 +76,7 @@ internal class GiveApprovalModel @Inject constructor( walletInteractionIcon = walletInterationIcon(userWallet), isApproveButtonEnabled = false, isApproveLoading = false, + isHoldToConfirm = params.isHoldToConfirm, ), ) diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt index 96fc768b88..38f5698135 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt @@ -11,4 +11,5 @@ internal data class GiveApprovalUM( @DrawableRes val walletInteractionIcon: Int?, val isApproveButtonEnabled: Boolean, val isApproveLoading: Boolean, + val isHoldToConfirm: Boolean = false, ) \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt index 2d3386f430..ce70ed4b1c 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt @@ -48,6 +48,7 @@ internal fun GiveApprovalContent( walletInteractionIcon: Int?, isApproveEnabled: Boolean, isApproveLoading: Boolean, + isHoldToConfirm: Boolean, onApproveClick: () -> Unit, onCancelClick: () -> Unit, onOpenLearnMoreAboutApproveClick: () -> Unit, @@ -81,16 +82,28 @@ internal fun GiveApprovalContent( SpacerH(height = TangemTheme.dimens.spacing20) - PrimaryButtonIconEnd( - text = stringResourceSafe(id = CommonUiR.string.common_approve), - iconResId = walletInteractionIcon, - showProgress = isApproveLoading, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing16), - onClick = onApproveClick, - enabled = isApproveEnabled, - ) + if (isHoldToConfirm) { + HoldToConfirmButton( + text = stringResourceSafe(id = CommonUiR.string.common_approve), + enabled = isApproveEnabled, + isLoading = isApproveLoading, + onConfirm = onApproveClick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + ) + } else { + PrimaryButtonIconEnd( + text = stringResourceSafe(id = CommonUiR.string.common_approve), + iconResId = walletInteractionIcon, + showProgress = isApproveLoading, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + onClick = onApproveClick, + enabled = isApproveEnabled, + ) + } SpacerH12() @@ -309,6 +322,7 @@ private fun GiveApprovalContentPreview( walletInteractionIcon = params.walletInteractionIcon, isApproveEnabled = params.isApproveEnabled, isApproveLoading = params.isApproveLoading, + isHoldToConfirm = false, onApproveClick = {}, onCancelClick = {}, onOpenLearnMoreAboutApproveClick = {}, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index a45bcd44b5..f4b678091c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -1497,6 +1497,7 @@ internal class StakingModel @Inject constructor( id = R.string.give_permission_staking_subtitle, formatArgs = wrappedList(cryptoCurrencyStatus.currency.symbol), ), + isHoldToConfirm = value.shouldShowHoldToConfirmButton, callback = approvalCallback, ) } 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 00b3379362..725c5869e8 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 @@ -44,8 +44,10 @@ internal class SetButtonsStateTransformer( val isConfirmation = prevState.currentStep == StakingStep.Confirmation val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED + val isApprovalRequired = prevState.isApprovalRequired() - val isHoldToConfirm = prevState.shouldShowHoldToConfirmButton && isConfirmation && !isCompleted + val isHoldToConfirm = prevState.shouldShowHoldToConfirmButton && + isConfirmation && !isCompleted && !isApprovalRequired val isIconVisible = isConfirmation && !isCompleted && !isHoldToConfirm val isPrimaryButtonDisabled = prevState.isPrimaryButtonDisabled() return NavigationButton( @@ -114,14 +116,9 @@ internal class SetButtonsStateTransformer( private fun StakingUiState.getConfirmationButtonText(): TextReference { val confirmationState = confirmationState as? StakingStates.ConfirmationState.Data ?: return resourceReference(R.string.common_close) - val amountState = amountState as? AmountState.Data - ?: return resourceReference(R.string.common_close) - if (actionType is StakingActionCommonType.Enter) { - val amount = amountState.amountTextField.cryptoAmount.value.orZero() - if (confirmationState.isApprovalNeeded && confirmationState.allowance < amount) { - return resourceReference(R.string.give_permission_title) - } + if (isApprovalRequired()) { + return resourceReference(R.string.give_permission_title) } return getBaseActionText(confirmationState) @@ -151,18 +148,13 @@ internal class SetButtonsStateTransformer( private fun StakingUiState.onConfirmationClick() { val confirmationState = confirmationState as? StakingStates.ConfirmationState.Data - val amountState = amountState as? AmountState.Data - if (confirmationState != null && amountState != null) { + if (confirmationState != null) { if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { clickIntents.onNextClick() + } else if (isApprovalRequired()) { + clickIntents.showApprovalBottomSheet() } else { - val amount = amountState.amountTextField.cryptoAmount.value.orZero() - val isEnterAction = actionType is StakingActionCommonType.Enter - if (isEnterAction && confirmationState.isApprovalNeeded && confirmationState.allowance < amount) { - clickIntents.showApprovalBottomSheet() - } else { - clickIntents.onActionClick() - } + clickIntents.onActionClick() } } else { clickIntents.onBackClick() @@ -177,6 +169,15 @@ internal class SetButtonsStateTransformer( return !hasNotStaking && isCardano && currentStep == StakingStep.InitialInfo } + private fun StakingUiState.isApprovalRequired(): Boolean { + val confirmState = confirmationState as? StakingStates.ConfirmationState.Data ?: return false + val amountState = amountState as? AmountState.Data ?: return false + val amount = amountState.amountTextField.cryptoAmount.value.orZero() + return actionType is StakingActionCommonType.Enter && + confirmState.isApprovalNeeded && + confirmState.allowance < amount + } + private fun StakingUiState.isButtonEnabled(): Boolean { return when (currentStep) { StakingStep.InitialInfo -> initialInfoState.isPrimaryButtonEnabled From 9a6b9f4b82857bad23d3534fbbfec24dbe02fd58 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 31 Mar 2026 11:37:36 +0400 Subject: [PATCH 41/75] Updated on 2026-08-14 --- .../main/java/com/tangem/feature/swap/DefaultSwapComponent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index 7035f014b0..5b770b9664 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -204,7 +204,7 @@ internal class DefaultSwapComponent @AssistedInject constructor( userWalletId = params.userWalletId, cryptoCurrencyStatus = fromCryptoCurrency, feeCryptoCurrencyStatus = feeCryptoCurrency, - amount = permissionState.amount, + amount = model.dataState.amount.orEmpty(), spenderAddress = requireNotNull(model.dataState.approveDataModel).spenderAddress, subtitle = resourceReference( id = R.string.give_permission_swap_subtitle, From d465cafc8d35174db69570910c729dc144767772 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Mar 2026 20:59:04 +0300 Subject: [PATCH 42/75] Updated on 2026-08-14 --- .../main/java/com/tangem/feature/swap/DefaultSwapComponent.kt | 1 + .../src/main/java/com/tangem/feature/swap/model/SwapModel.kt | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index 5b770b9664..7d480eb2cf 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -210,6 +210,7 @@ internal class DefaultSwapComponent @AssistedInject constructor( id = R.string.give_permission_swap_subtitle, formatArgs = wrappedList(providerName, permissionState.currency), ), + isHoldToConfirm = model.isHoldToConfirmEnabled, callback = model.approvalCallback, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 53cf08c449..6cc3e25770 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -53,6 +53,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.pay.WithdrawalResult import com.tangem.domain.promo.ShouldShowStoriesUseCase import com.tangem.domain.promo.models.StoryContentIds @@ -164,6 +165,9 @@ internal class SwapModel @Inject constructor( } private val swapInteractor = swapInteractorFactory.create(userWalletId) + val isHoldToConfirmEnabled: Boolean = + holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled && userWallet.isHotWallet + private lateinit var initialFromStatus: CryptoCurrencyStatus private var initialToStatus: CryptoCurrencyStatus? = null From 149a638c0dcba900e3ec50b2ad7bdb2a6b64c769 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 31 Mar 2026 16:03:56 +0500 Subject: [PATCH 43/75] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 17 +- data/transaction/build.gradle.kts | 1 + .../transaction/DefaultAllowanceRepository.kt | 73 +++++ .../DefaultTransactionRepository.kt | 23 -- .../transaction/di/TransactionDataModule.kt | 24 +- .../DefaultAllowanceRepositoryTest.kt | 305 ++++++++++++++++++ .../domain/transaction/AllowanceRepository.kt | 46 +++ .../transaction/TransactionRepository.kt | 8 - .../transaction/models/AllowanceInfo.kt | 25 ++ .../usecase/GetAllowanceInfoUseCase.kt | 32 ++ .../usecase/GetAllowanceUseCase.kt | 11 +- .../v2/impl/amount/model/SwapAmountModel.kt | 24 +- .../feature/swap/DefaultSwapRepository.kt | 38 +-- .../feature/swap/domain/SwapInteractorImpl.kt | 31 +- .../feature/swap/domain/api/SwapRepository.kt | 13 - .../com/tangem/lib/crypto/BlockchainUtils.kt | 10 + 16 files changed, 542 insertions(+), 139 deletions(-) create mode 100644 data/transaction/src/main/java/com/tangem/data/transaction/DefaultAllowanceRepository.kt create mode 100644 data/transaction/src/test/kotlin/com/tangem/data/transaction/DefaultAllowanceRepositoryTest.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/AllowanceRepository.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/models/AllowanceInfo.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetAllowanceInfoUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 61d03da6bf..9b3b5503e7 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -4,7 +4,6 @@ import com.tangem.data.wallets.hot.TangemHotWalletSigner import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.card.repository.CardSdkConfigRepository -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier @@ -12,13 +11,11 @@ import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.repository.CurrencyChecksRepository -import com.tangem.domain.transaction.FeeRepository -import com.tangem.domain.transaction.GaslessTransactionRepository -import com.tangem.domain.transaction.TransactionRepository -import com.tangem.domain.transaction.WalletAddressServiceRepository +import com.tangem.domain.transaction.* import com.tangem.domain.transaction.usecase.* import com.tangem.domain.transaction.usecase.gasless.* import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.AppCoroutineScope import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -175,8 +172,14 @@ internal object TransactionDomainModule { @Provides @Singleton - fun provideGetAllowanceUseCase(transactionRepository: TransactionRepository): GetAllowanceUseCase { - return GetAllowanceUseCase(transactionRepository) + fun provideGetAllowanceUseCase(allowanceRepository: AllowanceRepository): GetAllowanceUseCase { + return GetAllowanceUseCase(allowanceRepository) + } + + @Provides + @Singleton + fun provideGetAllowanceInfoUseCase(allowanceRepository: AllowanceRepository): GetAllowanceInfoUseCase { + return GetAllowanceInfoUseCase(allowanceRepository) } @Provides diff --git a/data/transaction/build.gradle.kts b/data/transaction/build.gradle.kts index a372c747a8..8aa184a6a6 100644 --- a/data/transaction/build.gradle.kts +++ b/data/transaction/build.gradle.kts @@ -29,6 +29,7 @@ dependencies { /** Domain */ implementation(projects.libs.blockchainSdk) + implementation(projects.libs.crypto) implementation(projects.domain.legacy) implementation(projects.domain.walletManager) implementation(projects.domain.wallets.models) diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultAllowanceRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultAllowanceRepository.kt new file mode 100644 index 0000000000..29395fce20 --- /dev/null +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultAllowanceRepository.kt @@ -0,0 +1,73 @@ +package com.tangem.data.transaction + +import com.tangem.blockchain.common.Approver +import com.tangem.blockchain.common.Token +import com.tangem.blockchainsdk.utils.toBlockchain +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.AllowanceRepository +import com.tangem.domain.transaction.models.AllowanceInfo +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import java.math.BigDecimal + +internal class DefaultAllowanceRepository( + private val walletManagersFacade: WalletManagersFacade, + private val dispatchers: CoroutineDispatcherProvider, +) : AllowanceRepository { + + override suspend fun getAllowanceInfo( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + spenderAddress: String, + requiredAmount: BigDecimal, + ): AllowanceInfo { + if (cryptoCurrency !is CryptoCurrency.Token) { + error("CryptoCurrency must be of type Token") + } + + val allowance = getAllowance( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + spenderAddress = spenderAddress, + ) + + return when { + allowance >= requiredAmount -> AllowanceInfo.Enough(allowance) + allowance > BigDecimal.ZERO && allowance < requiredAmount && + BlockchainUtils.isTetherInEthereum( + blockchainId = cryptoCurrency.network.rawId, + contractAddress = cryptoCurrency.contractAddress, + ) -> AllowanceInfo.ResetNeeded(allowance, requiredAmount) + else -> AllowanceInfo.NotEnough(allowance, requiredAmount) + } + } + + override suspend fun getAllowance( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + spenderAddress: String, + ): BigDecimal = withContext(dispatchers.io) { + if (cryptoCurrency !is CryptoCurrency.Token) { + error("CryptoCurrency must be of type Token") + } + + val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, cryptoCurrency.network) + val blockchain = cryptoCurrency.network.toBlockchain() + val allowanceResult = (walletManager as? Approver)?.getAllowance( + spenderAddress, + Token( + symbol = blockchain.currency, + contractAddress = cryptoCurrency.contractAddress, + decimals = cryptoCurrency.decimals, + ), + ) ?: error("Cannot cast to Approver") + + allowanceResult.fold( + onSuccess = { it }, + onFailure = { error(it) }, + ) + } +} \ No newline at end of file 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 ba4141909d..06da7b3fa9 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 @@ -25,7 +25,6 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.OperationType import com.tangem.datasource.api.tangemTech.models.TransactionEventBody import com.tangem.datasource.local.walletmanager.WalletManagersStore -import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.transaction.TransactionRepository @@ -332,28 +331,6 @@ internal class DefaultTransactionRepository( } } - override suspend fun getAllowance( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency.Token, - spenderAddress: String, - ): BigDecimal { - val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, cryptoCurrency.network) - val blockchain = cryptoCurrency.network.toBlockchain() - val allowanceResult = (walletManager as? Approver)?.getAllowance( - spenderAddress, - Token( - symbol = blockchain.currency, - contractAddress = cryptoCurrency.contractAddress, - decimals = cryptoCurrency.decimals, - ), - ) ?: error("Cannot cast to Approver") - - return allowanceResult.fold( - onSuccess = { it }, - onFailure = { error(it) }, - ) - } - @Suppress("CyclomaticComplexMethod") private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { val blockchain = Blockchain.fromId(networkId) diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt index f3567dbef8..9f9f0ecbd8 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt @@ -1,22 +1,14 @@ package com.tangem.data.transaction.di import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory -import com.tangem.data.transaction.DefaultFeeRepository -import com.tangem.data.transaction.DefaultGaslessTransactionRepository -import com.tangem.data.transaction.DefaultMemoValidatorFacade -import com.tangem.data.transaction.DefaultTransactionRepository -import com.tangem.data.transaction.DefaultWalletAddressServiceRepository +import com.tangem.data.transaction.* import com.tangem.data.transaction.error.DefaultFeeErrorResolver import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.datasource.api.gasless.GaslessTxServiceApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.demo.models.DemoConfig -import com.tangem.domain.transaction.FeeRepository -import com.tangem.domain.transaction.GaslessTransactionRepository -import com.tangem.domain.transaction.MemoValidatorFacade -import com.tangem.domain.transaction.TransactionRepository -import com.tangem.domain.transaction.WalletAddressServiceRepository +import com.tangem.domain.transaction.* import com.tangem.domain.transaction.error.FeeErrorResolver import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -100,4 +92,16 @@ internal object TransactionDataModule { responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, ) } + + @Provides + @Singleton + fun provideAllowanceRepository( + walletManagersFacade: WalletManagersFacade, + dispatchers: CoroutineDispatcherProvider, + ): AllowanceRepository { + return DefaultAllowanceRepository( + walletManagersFacade = walletManagersFacade, + dispatchers = dispatchers, + ) + } } \ No newline at end of file diff --git a/data/transaction/src/test/kotlin/com/tangem/data/transaction/DefaultAllowanceRepositoryTest.kt b/data/transaction/src/test/kotlin/com/tangem/data/transaction/DefaultAllowanceRepositoryTest.kt new file mode 100644 index 0000000000..0211c42b33 --- /dev/null +++ b/data/transaction/src/test/kotlin/com/tangem/data/transaction/DefaultAllowanceRepositoryTest.kt @@ -0,0 +1,305 @@ +package com.tangem.data.transaction + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.Approver +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.WalletManager +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.models.AllowanceInfo +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.math.BigDecimal + +class DefaultAllowanceRepositoryTest { + + private val userWalletId = UserWalletId(stringValue = "1234567890ABCDEF") + private val spenderAddress = "0xSpender" + + private val approverWalletManager: WalletManager = + mockk(moreInterfaces = arrayOf(Approver::class)) + + private val walletManagersFacade: WalletManagersFacade = mockk { + coEvery { getOrCreateWalletManager(userWalletId, any()) } returns approverWalletManager + } + + private val dispatchers = TestingCoroutineDispatcherProvider() + private lateinit var repository: DefaultAllowanceRepository + + @BeforeEach + fun setup() { + repository = DefaultAllowanceRepository( + walletManagersFacade = walletManagersFacade, + dispatchers = dispatchers, + ) + } + + // region getAllowance + + @Nested + inner class GetAllowanceTests { + + @Test + fun `throws when cryptoCurrency is Coin`() = runTest { + val coin = buildCoin() + + assertThrows { + repository.getAllowance(userWalletId, coin, spenderAddress) + } + } + + @Test + fun `throws when walletManager is null`() = runTest { + val token = buildToken() + coEvery { + walletManagersFacade.getOrCreateWalletManager(userWalletId, token.network) + } returns null + + assertThrows { + repository.getAllowance(userWalletId, token, spenderAddress) + } + } + + @Test + fun `throws when walletManager is not Approver`() = runTest { + val token = buildToken() + val nonApproverWalletManager: WalletManager = mockk() + coEvery { + walletManagersFacade.getOrCreateWalletManager(userWalletId, token.network) + } returns nonApproverWalletManager + + assertThrows { + repository.getAllowance(userWalletId, token, spenderAddress) + } + } + + @Test + fun `returns allowance on success`() = runTest { + val token = buildToken() + val expected = BigDecimal("100") + + coEvery { + (approverWalletManager as Approver).getAllowance(spenderAddress, any()) + } returns Result.success(expected) + + val result = repository.getAllowance(userWalletId, token, spenderAddress) + + assertThat(result).isEqualTo(expected) + } + + @Test + fun `throws when approver returns failure`() = runTest { + val token = buildToken() + coEvery { + (approverWalletManager as Approver).getAllowance(spenderAddress, any()) + } returns Result.failure(RuntimeException("rpc error")) + + assertThrows { + repository.getAllowance(userWalletId, token, spenderAddress) + } + } + } + + // endregion + + // region getAllowanceInfo + + @Nested + inner class GetAllowanceInfoTests { + + @Test + fun `throws when cryptoCurrency is Coin`() = runTest { + val coin = buildCoin() + + assertThrows { + repository.getAllowanceInfo(userWalletId, coin, spenderAddress, BigDecimal.ONE) + } + } + + @Test + fun `returns Enough when allowance equals required amount`() = runTest { + val token = buildToken(rawNetworkId = "polygon", rawCurrencyId = "usd-coin") + val amount = BigDecimal("100") + + coEvery { + (approverWalletManager as Approver).getAllowance(spenderAddress, any()) + } returns Result.success(amount) + + val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, amount) + + assertThat(result).isInstanceOf(AllowanceInfo.Enough::class.java) + assertThat((result as AllowanceInfo.Enough).allowance).isEqualTo(amount) + } + + @Test + fun `returns Enough when allowance exceeds required amount`() = runTest { + val token = buildToken(rawNetworkId = "polygon", rawCurrencyId = "usd-coin") + + coEvery { + (approverWalletManager as Approver).getAllowance(spenderAddress, any()) + } returns Result.success(BigDecimal("200")) + + val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("100")) + + assertThat(result).isInstanceOf(AllowanceInfo.Enough::class.java) + assertThat((result as AllowanceInfo.Enough).allowance).isEqualTo(BigDecimal("200")) + } + + @Test + fun `returns NotEnough when allowance is zero`() = runTest { + val token = buildToken(rawNetworkId = "ethereum", rawCurrencyId = "tether") + + coEvery { + (approverWalletManager as Approver).getAllowance(spenderAddress, any()) + } returns Result.success(BigDecimal.ZERO) + + val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("50")) + + assertThat(result).isInstanceOf(AllowanceInfo.NotEnough::class.java) + result as AllowanceInfo.NotEnough + assertThat(result.allowance).isEqualTo(BigDecimal.ZERO) + assertThat(result.requiredAmount).isEqualTo(BigDecimal("50")) + } + + @Test + fun `returns NotEnough when partial allowance for non-tether token`() = runTest { + val token = buildToken(rawNetworkId = "ethereum", rawCurrencyId = "usd-coin") + + coEvery { + (approverWalletManager as Approver).getAllowance(spenderAddress, any()) + } returns Result.success(BigDecimal("30")) + + val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("100")) + + assertThat(result).isInstanceOf(AllowanceInfo.NotEnough::class.java) + result as AllowanceInfo.NotEnough + assertThat(result.allowance).isEqualTo(BigDecimal("30")) + assertThat(result.requiredAmount).isEqualTo(BigDecimal("100")) + } + + @Test + fun `returns NotEnough when partial allowance for tether on non-ethereum network`() = runTest { + val token = buildToken(rawNetworkId = "polygon", rawCurrencyId = "tether") + + coEvery { + (approverWalletManager as Approver).getAllowance(spenderAddress, any()) + } returns Result.success(BigDecimal("30")) + + val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("100")) + + assertThat(result).isInstanceOf(AllowanceInfo.NotEnough::class.java) + } + + @Test + fun `returns ResetNeeded when partial allowance for tether on ethereum`() = runTest { + val token = buildToken(rawNetworkId = "ETH", rawCurrencyId = "tether") + + coEvery { + (approverWalletManager as Approver).getAllowance(spenderAddress, any()) + } returns Result.success(BigDecimal("30")) + + val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("100")) + + assertThat(result).isInstanceOf(AllowanceInfo.ResetNeeded::class.java) + result as AllowanceInfo.ResetNeeded + assertThat(result.allowance).isEqualTo(BigDecimal("30")) + assertThat(result.requiredAmount).isEqualTo(BigDecimal("100")) + } + + @Test + fun `returns ResetNeeded when partial allowance for tether on ethereum testnet`() = runTest { + val token = buildToken(rawNetworkId = "ETH/test", rawCurrencyId = "tether") + + coEvery { + (approverWalletManager as Approver).getAllowance(spenderAddress, any()) + } returns Result.success(BigDecimal("10")) + + val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("50")) + + assertThat(result).isInstanceOf(AllowanceInfo.ResetNeeded::class.java) + } + + @Test + fun `returns Enough for tether on ethereum when allowance is sufficient`() = runTest { + val token = buildToken(rawNetworkId = "ethereum", rawCurrencyId = "tether") + + coEvery { + (approverWalletManager as Approver).getAllowance(spenderAddress, any()) + } returns Result.success(BigDecimal("100")) + + val result = repository.getAllowanceInfo(userWalletId, token, spenderAddress, BigDecimal("100")) + + assertThat(result).isInstanceOf(AllowanceInfo.Enough::class.java) + } + } + + // endregion + + // region Helpers + + private fun buildNetwork(rawNetworkId: String): Network { + val derivationPath = Network.DerivationPath.None + return Network( + id = Network.ID(Network.RawID(rawNetworkId), derivationPath), + backendId = rawNetworkId, + name = rawNetworkId.replaceFirstChar { it.uppercase() }, + currencySymbol = "ETH", + derivationPath = derivationPath, + isTestnet = rawNetworkId.contains("test"), + standardType = Network.StandardType.ERC20, + hasFiatFeeRate = false, + canHandleTokens = true, + transactionExtrasType = Network.TransactionExtrasType.NONE, + nameResolvingType = Network.NameResolvingType.NONE, + ) + } + + private fun buildToken( + rawNetworkId: String = "ETH", + rawCurrencyId: String = "tether", + contractAddress: String = "0xdAC17F958D2ee523a2206206994597C13D831ec7", + ): CryptoCurrency.Token { + val network = buildNetwork(rawNetworkId) + return CryptoCurrency.Token( + id = CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId), + suffix = CryptoCurrency.ID.Suffix.RawID(rawCurrencyId), + ), + network = network, + name = "Token", + symbol = "TKN", + decimals = 6, + iconUrl = null, + isCustom = false, + contractAddress = contractAddress, + ) + } + + private fun buildCoin(rawNetworkId: String = "ethereum"): CryptoCurrency.Coin { + val network = buildNetwork(rawNetworkId) + return CryptoCurrency.Coin( + id = CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkId(rawNetworkId), + suffix = CryptoCurrency.ID.Suffix.RawID("ethereum"), + ), + network = network, + name = "Ethereum", + symbol = "ETH", + decimals = 18, + iconUrl = null, + isCustom = false, + ) + } + + // endregion +} diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/AllowanceRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/AllowanceRepository.kt new file mode 100644 index 0000000000..8f7075c563 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/AllowanceRepository.kt @@ -0,0 +1,46 @@ +package com.tangem.domain.transaction + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.models.AllowanceInfo +import java.math.BigDecimal + +/** + * Repository interface for managing token allowances in the context of blockchain transactions. + */ +interface AllowanceRepository { + + /** + * Retrieves the allowance information for a specific spender and required amount. + * + * @param userWalletId The ID of the user's wallet. + * @param cryptoCurrency The cryptocurrency for which the allowance is being checked (must be a token). + * @param spenderAddress The address of the spender for whom the allowance is being checked. + * @param requiredAmount The amount that is required for the transaction. + * + * @return An [AllowanceInfo] object that indicates whether the current allowance. + * @throws IllegalStateException if the provided [cryptoCurrency] is not a token. + */ + suspend fun getAllowanceInfo( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + spenderAddress: String, + requiredAmount: BigDecimal, + ): AllowanceInfo + + /** + * Retrieves the current allowance for a specific spender. + * + * @param userWalletId The ID of the user's wallet. + * @param cryptoCurrency The cryptocurrency for which the allowance is being checked (must be a token). + * @param spenderAddress The address of the spender for whom the allowance is being checked. + * + * @return The current allowance as a [BigDecimal]. + * @throws IllegalStateException if the provided [cryptoCurrency] is not a token. + */ + suspend fun getAllowance( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + spenderAddress: String, + ): BigDecimal +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index 74958179f4..e26582e022 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -6,11 +6,9 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionSendResult import com.tangem.blockchain.common.transaction.TransactionsSendResult import com.tangem.blockchain.nft.models.NFTAsset -import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.transaction.models.EventTransactionTypeDto -import java.math.BigDecimal import java.math.BigInteger interface TransactionRepository { @@ -91,12 +89,6 @@ interface TransactionRepository { gasLimit: BigInteger?, ): TransactionExtras - suspend fun getAllowance( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency.Token, - spenderAddress: String, - ): BigDecimal - suspend fun prepareForSend( transactionData: TransactionData, signer: TransactionSigner, diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/models/AllowanceInfo.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/models/AllowanceInfo.kt new file mode 100644 index 0000000000..43ec01bf10 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/models/AllowanceInfo.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.transaction.models + +import java.math.BigDecimal + +/** + * Model that represents the allowance information for a specific spender and required amount. + */ +sealed class AllowanceInfo { + + /** + * Represents a state where the current allowance is sufficient to cover the required amount. + */ + data class Enough(val allowance: BigDecimal) : AllowanceInfo() + + /** + * Represents a state where the current allowance is insufficient to cover the required amount. + */ + data class NotEnough(val allowance: BigDecimal, val requiredAmount: BigDecimal) : AllowanceInfo() + + /** + * Represents a state where the current allowance is insufficient, + * but it must be reset to cover the required amount (specific to certain tokens like Tether in Ethereum). + */ + data class ResetNeeded(val allowance: BigDecimal, val requiredAmount: BigDecimal) : AllowanceInfo() +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetAllowanceInfoUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetAllowanceInfoUseCase.kt new file mode 100644 index 0000000000..93162b472a --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetAllowanceInfoUseCase.kt @@ -0,0 +1,32 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.AllowanceRepository +import com.tangem.domain.transaction.models.AllowanceInfo +import java.math.BigDecimal + +/** + * Use case for retrieving the allowance information for a specific spender and required amount. + */ +class GetAllowanceInfoUseCase( + private val allowanceRepository: AllowanceRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + spenderAddress: String, + requiredAmount: BigDecimal, + ): Either { + return Either.catch { + allowanceRepository.getAllowanceInfo( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + spenderAddress = spenderAddress, + requiredAmount = requiredAmount, + ) + } + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetAllowanceUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetAllowanceUseCase.kt index 2f09a5bc8d..ccc3a90c5a 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetAllowanceUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetAllowanceUseCase.kt @@ -2,12 +2,15 @@ package com.tangem.domain.transaction.usecase import arrow.core.Either import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.AllowanceRepository import java.math.BigDecimal +/** + * Use case for retrieving the current allowance for a specific spender. + */ class GetAllowanceUseCase( - private val transactionRepository: TransactionRepository, + private val allowanceRepository: AllowanceRepository, ) { suspend operator fun invoke( @@ -16,9 +19,9 @@ class GetAllowanceUseCase( spenderAddress: String, ): Either { return Either.catch { - transactionRepository.getAllowance( + allowanceRepository.getAllowance( userWalletId = userWalletId, - cryptoCurrency = cryptoCurrency as CryptoCurrency.Token, + cryptoCurrency = cryptoCurrency, spenderAddress = spenderAddress, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 90eea6d0fa..6123e22384 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -34,7 +34,8 @@ import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase import com.tangem.domain.swap.usecase.SelectInitialPairUseCase import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase -import com.tangem.domain.transaction.usecase.GetAllowanceUseCase +import com.tangem.domain.transaction.models.AllowanceInfo +import com.tangem.domain.transaction.usecase.GetAllowanceInfoUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.send.v2.api.subcomponents.amount.analytics.CommonSendAmountAnalyticEvents import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger @@ -81,7 +82,7 @@ internal class SwapAmountModel @Inject constructor( private val selectInitialPairUseCase: SelectInitialPairUseCase, private val getSwapQuoteUseCase: GetSwapQuoteUseCase, private val swapChooseTokenNetworkListener: SwapChooseTokenNetworkListener, - private val getAllowanceUseCase: GetAllowanceUseCase, + private val getAllowanceInfoUseCase: GetAllowanceInfoUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserCountryUseCase: GetUserCountryUseCase, private val swapBestRateAnimationStore: SwapBestRateAnimationStore, @@ -805,18 +806,15 @@ internal class SwapAmountModel @Inject constructor( } private suspend fun checkAllowance(state: SwapAmountUM.Content, quote: SwapQuoteModel): Boolean { - val allowanceContract = quote.allowanceContract - val allowance = if (allowanceContract != null) { - getAllowanceUseCase( - userWalletId = userWallet.walletId, - cryptoCurrency = state.primaryCryptoCurrencyStatus.currency, - spenderAddress = allowanceContract, - ).getOrNull() - } else { - BigDecimal.ZERO - } + val allowanceContract = quote.allowanceContract ?: return false + val allowance = getAllowanceInfoUseCase( + userWalletId = userWallet.walletId, + cryptoCurrency = state.primaryCryptoCurrencyStatus.currency, + spenderAddress = allowanceContract, + requiredAmount = state.primaryCryptoCurrencyStatus.value.amount.orZero(), + ).getOrNull() - return allowance.orZero() < state.primaryCryptoCurrencyStatus.value.amount.orZero() + return allowance !is AllowanceInfo.Enough } private fun saveResult() { diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index deff8ff1f2..e27cb8705f 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -6,10 +6,6 @@ import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.right import com.squareup.moshi.Moshi -import com.tangem.blockchain.common.Approver -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow @@ -27,7 +23,6 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.api.SwapRepository @@ -36,12 +31,11 @@ import com.tangem.feature.swap.domain.models.ExpressException import com.tangem.feature.swap.domain.models.createFromAmountWithOffset import com.tangem.feature.swap.domain.models.domain.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext -import com.tangem.utils.logging.TangemLogger import java.io.IOException -import java.math.BigDecimal import java.util.UUID import com.tangem.datasource.api.express.models.request.LeastTokenInfo as NetworkLeastTokenInfo @@ -410,36 +404,6 @@ internal class DefaultSwapRepository( } } - override suspend fun getAllowance( - userWalletId: UserWalletId, - networkId: String, - derivationPath: String?, - tokenDecimalCount: Int, - tokenAddress: String, - spenderAddress: String, - ): BigDecimal { - val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val walletManager = walletManagersFacade.getOrCreateWalletManager( - userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = derivationPath, - ) - - val result = (walletManager as? Approver)?.getAllowance( - spenderAddress, - Token( - symbol = blockchain.currency, - contractAddress = tokenAddress, - decimals = tokenDecimalCount, - ), - ) ?: error("Cannot cast to Approver") - - return result.fold( - onSuccess = { it }, - onFailure = { error(it) }, - ) - } - private fun getDataError(ex: Exception): ExpressDataError { return if (ex is ApiResponseError.HttpException) { errorsDataConverter.convert(ex.errorBody.orEmpty()) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 405c0aa43b..7987443ea3 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -51,6 +51,7 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.models.AllowanceInfo import com.tangem.domain.transaction.models.TransactionFeeExtended import com.tangem.domain.transaction.usecase.* import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase @@ -112,6 +113,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val walletManagersFacade: WalletManagersFacade, + private val getAllowanceInfoUseCase: GetAllowanceInfoUseCase, @Assisted private val userWalletId: UserWalletId, ) : SwapInteractor { @@ -431,12 +433,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( val isAllowedToSpend = maybeQuotes.fold( ifRight = { quotes -> quotes.allowanceContract?.let { allowanceContract -> - isAllowedToSpend( - networkId = networkId, - fromToken = fromToken.currency, - amount = amount, + getAllowanceInfoUseCase( + userWalletId = userWalletId, + cryptoCurrency = fromToken.currency, spenderAddress = allowanceContract, - ) + requiredAmount = amount.value, + ).getOrNull() is AllowanceInfo.Enough } != false }, ifLeft = { false }, @@ -1268,25 +1270,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( ?: error("Unable to create network coin with ID: ${network.id}") } - private suspend fun isAllowedToSpend( - networkId: String, - fromToken: CryptoCurrency, - amount: SwapAmount, - spenderAddress: String, - ): Boolean { - if (fromToken is CryptoCurrency.Coin) return true - - val allowance = repository.getAllowance( - userWalletId = userWallet.walletId, - networkId = networkId, - derivationPath = fromToken.network.derivationPath.value, - tokenDecimalCount = fromToken.decimals, - tokenAddress = getTokenAddress(fromToken), - spenderAddress = spenderAddress, - ) - return allowance >= amount.value - } - private suspend fun createEmptyAmountState(): SwapState { val appCurrency = getSelectedAppCurrencyUseCase.unwrap() return SwapState.EmptyAmountState( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt index c9ba429d75..c197aac5a9 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt @@ -4,10 +4,8 @@ import arrow.core.Either import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.domain.* -import java.math.BigDecimal interface SwapRepository { @@ -41,17 +39,6 @@ interface SwapRepository { rateType: RateType, ): Either - @Suppress("LongParameterList") - @Throws(IllegalStateException::class) - suspend fun getAllowance( - userWalletId: UserWalletId, - networkId: String, - derivationPath: String?, - tokenDecimalCount: Int, - tokenAddress: String, - spenderAddress: String, - ): BigDecimal - @Suppress("LongParameterList") suspend fun getExchangeData( userWallet: UserWallet, 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 1be48e89c6..b3108e4f2e 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 @@ -23,6 +23,7 @@ object BlockchainUtils { private const val XRP_X_ADDRESS = 'X' private const val TERRA_CLASSIC_USD_COIN_ID = "terrausd" private const val TERRA_LUNA_CLASSIC_COIN_ID = "terra-luna" + private const val TETHER_CONTRACT_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7" const val SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES = 930 /** Decodes XRP Blockchain address */ @@ -217,4 +218,13 @@ object BlockchainUtils { coinId == TERRA_CLASSIC_USD_COIN_ID || coinId == TERRA_LUNA_CLASSIC_COIN_ID } + + /** + * Checks if the given coin is Tether on Ethereum network, which may require special handling in some cases. + */ + fun isTetherInEthereum(blockchainId: String, contractAddress: String): Boolean { + val blockchain = Blockchain.fromId(blockchainId) + return (blockchain == Blockchain.Ethereum || blockchain == Blockchain.EthereumTestnet) && + contractAddress.equals(TETHER_CONTRACT_ADDRESS, ignoreCase = true) + } } \ No newline at end of file From b517fe39dd46d31773d34a0560c974ee5d9fe5aa Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 31 Mar 2026 16:15:55 +0300 Subject: [PATCH 44/75] Updated on 2026-08-14 --- .../kotlin/com/tangem/common/BaseTestCase.kt | 1 + .../common/extensions/SemanticMatchers.kt | 10 +- .../com/tangem/scenarios/BaseScenarios.kt | 4 +- .../screens/AccountDetailsPageObject.kt | 24 ++ .../com/tangem/screens/DetailsPageObject.kt | 10 +- .../tangem/screens/MainScreenPageObject.kt | 41 +++ ...bject.kt => MainScreenTopBarPageObject.kt} | 8 +- .../tangem/screens/ManageTokensPageObject.kt | 55 +++ .../screens/UnableToHideDialogPageObject.kt | 42 +++ .../screens/WalletSettingsPageObject.kt | 21 +- .../kotlin/com/tangem/tests/DetailsTest.kt | 8 +- .../kotlin/com/tangem/tests/FeedbackTest.kt | 4 +- .../kotlin/com/tangem/tests/MainScreenTest.kt | 20 -- .../kotlin/com/tangem/tests/ScanCardTest.kt | 2 - .../com/tangem/tests/main/HideTokenTest.kt | 312 ++++++++++++++++++ .../com/tangem/tests/main/MainScreenTest.kt | 114 +++++++ .../com/tangem/tests/main/TokenListTest.kt | 51 +++ .../com/tangem/tests/main/WarningsTest.kt | 52 +++ .../send/amountScreen/SendAmountScreenTest.kt | 2 +- .../send/warnings/StellarWarningsTest.kt | 4 - .../common/ui/userwallet/UserWalletItem.kt | 5 +- .../tangem/core/ui/components/TangemSwitch.kt | 14 +- .../components/currency/icon/CurrencyIcon.kt | 6 +- .../core/ui/components/rows/RowComponents.kt | 9 +- .../ui/test/AccountDetailsScreenTestTags.kt | 5 + .../ui/test/ManageTokensScreenTestTags.kt | 8 + .../tangem/core/ui/test/MarketsTestTags.kt | 1 - .../com/tangem/core/ui/test/SwitchTestTags.kt | 5 + .../core/ui/test/TokenElementsTestTags.kt | 1 + .../ui/test/WalletSettingsScreenTestTags.kt | 1 + .../details/ui/AccountDetailsContent.kt | 5 +- .../managetokens/ui/ManageTokensScreen.kt | 6 +- 32 files changed, 790 insertions(+), 61 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/AccountDetailsPageObject.kt rename app/src/androidTest/kotlin/com/tangem/screens/{TopBarPageObject.kt => MainScreenTopBarPageObject.kt} (72%) create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/ManageTokensPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/UnableToHideDialogPageObject.kt delete mode 100644 app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/main/HideTokenTest.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/main/MainScreenTest.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/main/TokenListTest.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/main/WarningsTest.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/AccountDetailsScreenTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/ManageTokensScreenTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/SwitchTestTags.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 0f2fb8d3c6..3b89b53e54 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -167,6 +167,7 @@ abstract class BaseTestCase : TestCase( "SWAP_REDESIGN_ENABLED" to false, "ACCOUNTS_FEATURE_ENABLED" to true, "GASLESS_APPROVAL_ENABLED" to true, + "MAIN_SCREEN_QR_SCANNING_ENABLED" to true, ) ) } diff --git a/app/src/androidTest/kotlin/com/tangem/common/extensions/SemanticMatchers.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/SemanticMatchers.kt index 4bf468227f..276c0f9447 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/extensions/SemanticMatchers.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/SemanticMatchers.kt @@ -1,9 +1,17 @@ package com.tangem.common.extensions +import android.support.annotation.PluralsRes import androidx.compose.ui.test.SemanticsMatcher +import androidx.test.platform.app.InstrumentationRegistry import com.tangem.core.ui.utils.LazyListItemPositionSemantics import io.github.kakaocup.compose.node.builder.ViewBuilder fun ViewBuilder.hasLazyListItemPosition(position: Int) = apply { addSemanticsMatcher(SemanticsMatcher.expectValue(LazyListItemPositionSemantics, position)) -} \ No newline at end of file +} + +fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any): String = + InstrumentationRegistry.getInstrumentation() + .targetContext + .resources + .getQuantityString(resId, quantity, *formatArgs) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index 09f4b7bd1e..85e746d709 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -89,7 +89,7 @@ fun BaseTestCase.synchronizeAddresses( fun BaseTestCase.openDeviceSettingsScreen() { step("Open wallet details") { waitForIdle() - onTopBar { moreButton.clickWithAssertion() } + onMainScreenTopBar { moreButton.clickWithAssertion() } } step("Open 'Wallet settings' screen") { onDetailsScreen { walletNameButton.performClick() } @@ -101,7 +101,7 @@ fun BaseTestCase.openDeviceSettingsScreen() { fun BaseTestCase.openWalletConnectScreen() { step("Click 'More' button on TopBar") { - onTopBar { moreButton.clickWithAssertion() } + onMainScreenTopBar { moreButton.clickWithAssertion() } } step("Click on 'Wallet Connect' button") { onDetailsScreen { walletConnectButton.clickWithAssertion() } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/AccountDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/AccountDetailsPageObject.kt new file mode 100644 index 0000000000..b347ee4785 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/AccountDetailsPageObject.kt @@ -0,0 +1,24 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.AccountDetailsScreenTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode + +class AccountDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val topAppBarBackButton: KNode = child { + hasTestTag(TopAppBarTestTags.CLOSE_BUTTON) + } + + val manageTokensButton: KNode = child { + hasTestTag(AccountDetailsScreenTestTags.MANAGE_TOKENS_BUTTON) + } +} + +internal fun BaseTestCase.onAccountDetails(function: AccountDetailsPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt index 4c172531a0..69a1652732 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DetailsPageObject.kt @@ -3,6 +3,7 @@ package com.tangem.screens import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase import com.tangem.core.ui.test.DetailsScreenTestTags +import com.tangem.core.ui.test.TopAppBarTestTags import com.tangem.wallet.R import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen @@ -10,10 +11,11 @@ import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.kakao.common.utilities.getResourceString class DetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : - ComposeScreen( - semanticsProvider = semanticsProvider, - viewBuilderAction = { hasTestTag(DetailsScreenTestTags.SCREEN_CONTAINER) } - ) { + ComposeScreen(semanticsProvider = semanticsProvider) { + + val topAppBarBackButton: KNode = child { + hasTestTag(TopAppBarTestTags.CLOSE_BUTTON) + } val walletConnectButton: KNode = child { hasTestTag(DetailsScreenTestTags.SCREEN_ITEM) diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt index 56036ef56a..79bc25c0f5 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import androidx.compose.ui.test.hasAnyAncestor import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.getQuantityString import com.tangem.common.extensions.hasLazyListItemPosition import com.tangem.common.utils.LazyListItemNode import com.tangem.core.ui.test.* @@ -154,6 +155,30 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) useUnmergedTree = true } + val missingAddressNotificationIcon: KNode = child { + hasAnySibling(withText(getResourceString(R.string.warning_missing_derivation_title))) + hasTestTag(NotificationTestTags.ICON) + useUnmergedTree = true + } + + val missingAddressNotificationTitle: KNode = child { + hasTestTag(NotificationTestTags.TITLE) + hasText(getResourceString(R.string.warning_missing_derivation_title)) + useUnmergedTree = true + } + + fun missingAddressNotificationMessage(networkCount: Int): KNode = child { + hasTestTag(NotificationTestTags.MESSAGE) + hasText( + getQuantityString( + R.plurals.warning_missing_derivation_message, + networkCount, + networkCount + ) + ) + useUnmergedTree = true + } + val totalBalanceContainer: KNode = child { hasTestTag(MainScreenTestTags.WALLET_LIST_ITEM) } @@ -189,6 +214,11 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) hasText(getResourceString(CoreUiR.string.wallet_notification_address_copied)) } + val organizeTokensButtonNode: KNode = child { + hasTestTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON) + useUnmergedTree = true + } + /** * Find token list item with title and address */ @@ -203,6 +233,17 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) } } + @OptIn(ExperimentalTestApi::class) + fun tokenWithCustomDerivationIcon(tokenTitle: String): KNode { + return lazyList.childWith { + hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM) + hasText(tokenTitle) + }.child { + hasTestTag(TokenElementsTestTags.TOKEN_CUSTOM_DERIVATION_ICON) + useUnmergedTree = true + } + } + @OptIn(ExperimentalTestApi::class) fun organizeTokensButton(): KNode { return lazyList.childWith { diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TopBarPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenTopBarPageObject.kt similarity index 72% rename from app/src/androidTest/kotlin/com/tangem/screens/TopBarPageObject.kt rename to app/src/androidTest/kotlin/com/tangem/screens/MainScreenTopBarPageObject.kt index cb347bb51b..db7e74bef5 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/TopBarPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenTopBarPageObject.kt @@ -7,16 +7,18 @@ import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.KNode -class TopBarPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : - ComposeScreen( +class MainScreenTopBarPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen( semanticsProvider = semanticsProvider, viewBuilderAction = { hasTestTag(MainScreenTestTags.TOP_BAR) } ) { + val moreButton: KNode = child { hasTestTag(MainScreenTestTags.MORE_BUTTON) + hasPosition(1) useUnmergedTree = true } } -internal fun BaseTestCase.onTopBar(function: TopBarPageObject.() -> Unit) = +internal fun BaseTestCase.onMainScreenTopBar(function: MainScreenTopBarPageObject.() -> Unit) = onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/ManageTokensPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/ManageTokensPageObject.kt new file mode 100644 index 0000000000..2d9882fde8 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/ManageTokensPageObject.kt @@ -0,0 +1,55 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.common.R +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.BaseSearchBarTestTags +import com.tangem.core.ui.test.ManageTokensScreenTestTags +import com.tangem.core.ui.test.SwitchTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasTestTag as withTestTag +import androidx.compose.ui.test.hasText as withText +import androidx.compose.ui.test.hasAnySibling as withAnySibling +import androidx.compose.ui.test.hasAnyDescendant as withAnyDescendant +import androidx.compose.ui.test.hasAnyAncestor as withAnyAncestor + +class ManageTokensPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val searchField: KNode = child { + hasTestTag(BaseSearchBarTestTags.SEARCH_BAR) + } + + fun tokenItem(tokenName: String): KNode = child { + hasTestTag(ManageTokensScreenTestTags.TOKEN_ITEM) + hasText(tokenName) + } + + fun networkSwitch(networkName: String): KNode = child { + useUnmergedTree = true + addSemanticsMatcher( + withTestTag(SwitchTestTags.SWITCH) + .and( + withAnyAncestor( + withAnySibling( + withTestTag(ManageTokensScreenTestTags.NETWORK_NAME) + .and(withAnyDescendant(withText(networkName))) + ) + ) + ) + ) + } + + val saveButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.common_save)) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onManageTokensScreen(function: ManageTokensPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/UnableToHideDialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/UnableToHideDialogPageObject.kt new file mode 100644 index 0000000000..8d2d336ea7 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/UnableToHideDialogPageObject.kt @@ -0,0 +1,42 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.R +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.BaseDialogTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class UnableToHideDialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + fun unableToHideTokenTitle(tokenName: String): KNode = child { + hasTestTag(BaseDialogTestTags.TITLE) + hasText(getResourceString( + R.string.token_details_unable_hide_alert_title, + tokenName)) + } + + fun unableToHideTokenMessage(tokenName: String, tokenSymbol: String, networkName: String): KNode = child { + hasTestTag(BaseDialogTestTags.TEXT) + hasText( + getResourceString( + R.string.token_details_unable_hide_alert_message, + tokenName, + tokenSymbol, + networkName + ) + ) + } + + val okButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.common_ok)) + } +} + +internal fun BaseTestCase.onUnableToHideDialog(function: UnableToHideDialogPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt index e8b554846b..db6413a7f2 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletSettingsPageObject.kt @@ -2,18 +2,22 @@ package com.tangem.screens import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.TopAppBarTestTags import com.tangem.core.ui.test.WalletSettingsScreenTestTags import com.tangem.wallet.R import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasText as withText class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : - ComposeScreen( - semanticsProvider = semanticsProvider, - viewBuilderAction = { hasTestTag(WalletSettingsScreenTestTags.SCREEN_CONTAINER) } - ) { + ComposeScreen(semanticsProvider = semanticsProvider) { + + val topAppBarBackButton: KNode = child { + hasTestTag(TopAppBarTestTags.CLOSE_BUTTON) + } + private val walletSettingsItem: KNode = child { hasTestTag(WalletSettingsScreenTestTags.SCREEN_ITEM) } @@ -21,15 +25,24 @@ class WalletSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvi val linkMoreCardsButton: KNode = walletSettingsItem.child { hasText(getResourceString(R.string.details_row_title_create_backup)) } + val deviceSettingsButton: KNode = walletSettingsItem.child { hasText(getResourceString(R.string.card_settings_title)) } + val referralProgramButton: KNode = walletSettingsItem.child { hasText(getResourceString(R.string.details_referral_title)) } + val forgetWalletButton: KNode = walletSettingsItem.child { hasText(getResourceString(R.string.settings_forget_wallet)) } + + fun accountItem(accountName: String): KNode = walletSettingsItem.child { + hasTestTag(WalletSettingsScreenTestTags.USER_ACCOUNT_ITEM) + hasAnyDescendant(withText(accountName)) + useUnmergedTree = true + } } internal fun BaseTestCase.onWalletSettingsScreen(function: WalletSettingsPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt index 54604ff5cf..7b980f10d1 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt @@ -19,7 +19,7 @@ class DetailsTest : BaseTestCase() { step("Open 'Main Screen'") { openMainScreen() } - onTopBar { + onMainScreenTopBar { step("Open wallet details") { moreButton.clickWithAssertion() } @@ -66,7 +66,7 @@ class DetailsTest : BaseTestCase() { step("Open 'Main Screen'") { openMainScreen(productType = ProductType.Wallet2) } - onTopBar { + onMainScreenTopBar { step("Open wallet details") { moreButton.clickWithAssertion() } @@ -116,7 +116,7 @@ class DetailsTest : BaseTestCase() { step("Open 'Main Screen'") { openMainScreen(ProductType.Note) } - onTopBar { + onMainScreenTopBar { step("Open wallet details") { moreButton.clickWithAssertion() } @@ -163,7 +163,7 @@ class DetailsTest : BaseTestCase() { openMainScreen() } step("Open wallet details") { - onTopBar { moreButton.clickWithAssertion() } + onMainScreenTopBar { moreButton.clickWithAssertion() } } step("Open 'Wallet settings' screen") { onDetailsScreen { walletNameButton.clickWithAssertion() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt index 6232f3caf0..7db589cb4a 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt @@ -26,7 +26,7 @@ import com.tangem.screens.onSendConfirmScreen import com.tangem.screens.onSendScreen import com.tangem.screens.onStoriesScreen import com.tangem.screens.onTokenDetailsScreen -import com.tangem.screens.onTopBar +import com.tangem.screens.onMainScreenTopBar import com.tangem.tap.domain.sdk.mocks.MockProvider import com.tangem.tap.store import dagger.hilt.android.testing.HiltAndroidTest @@ -57,7 +57,7 @@ class FeedbackTest : BaseTestCase() { } step("Click 'More' button on TopBar") { waitForIdle() - onTopBar { moreButton.clickWithAssertion() } + onMainScreenTopBar { moreButton.clickWithAssertion() } } step("Click 'Contact support' button") { waitForIdle() diff --git a/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt deleted file mode 100644 index cf7f5747e6..0000000000 --- a/app/src/androidTest/kotlin/com/tangem/tests/MainScreenTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.tests - -import com.tangem.common.BaseTestCase -import com.tangem.scenarios.openMainScreen -import dagger.hilt.android.testing.HiltAndroidTest -import org.junit.Test - -@HiltAndroidTest -class MainScreenTest : BaseTestCase() { - - @Test - fun goToMain() { - setupHooks().run { - step("Open 'Main Screen'") { - openMainScreen() - } - } - } - -} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/ScanCardTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/ScanCardTest.kt index f0a394e72e..1b83533fcb 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/ScanCardTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/ScanCardTest.kt @@ -14,7 +14,6 @@ import com.tangem.tap.domain.sdk.mocks.content.* import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName -import org.junit.Ignore import org.junit.Test @HiltAndroidTest @@ -86,7 +85,6 @@ class ScanCardTest : BaseTestCase() { } } - @Ignore("TODO: [REDACTED_JIRA]") @AllureId("870") @DisplayName("Scan: Card with Ed25519 curve") @Test diff --git a/app/src/androidTest/kotlin/com/tangem/tests/main/HideTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/main/HideTokenTest.kt new file mode 100644 index 0000000000..a69ee3c9f4 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/main/HideTokenTest.kt @@ -0,0 +1,312 @@ +package com.tangem.tests.main + +import androidx.compose.ui.test.longClick +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.synchronizeAddresses +import com.tangem.screens.* +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class HideTokenTest : BaseTestCase() { + + @AllureId("3638") + @DisplayName("Main: hide token by long tap") + @Test + fun hideTokenByLongTapTest() { + val tokenTitle = "Polygon" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Long click on token with name: '$tokenTitle'") { + waitForIdle() + onMainScreen { + tokenWithTitleAndAddress(tokenTitle).performTouchInput { + longClick( + position = center, + durationMillis = 1000L + ) + } + } + } + step("Click on 'Hide token' button") { + onTokenActionsBottomSheet { hideTokenButton.performClick() } + } + step("Click 'Hide' button in dialog") { + onDialog { + dialogContainer.assertIsDisplayed() + okButton.clickWithAssertion() + } + } + step("Assert token: '$tokenTitle' is not displayed") { + onMainScreen { assertTokenDoesNotExist(tokenTitle) } + } + } + } + + @AllureId("3627") + @DisplayName("Main: hide token via Manage tokens") + @Test + fun hideTokenViaManageTokensTest() { + val tokenTitle = "Tether" + val networkTitle = "ETHEREUM" + val scenarioState = "USDT" + val accountName = "Main account" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open wallet details") { + waitForIdle() + onMainScreenTopBar { moreButton.clickWithAssertion() } + } + step("Open 'Wallet settings' screen") { + onDetailsScreen { walletNameButton.performClick() } + } + step("Click on account: '$accountName'") { + onWalletSettingsScreen { accountItem(accountName).performClick() } + } + step("Click on 'Manage tokens' button") { + onAccountDetails { manageTokensButton.performClick() } + } + step("Click on token: '$tokenTitle'") { + onManageTokensScreen { tokenItem(tokenTitle).performClick() } + } + step("Assert switch is on") { + onManageTokensScreen { networkSwitch(networkTitle).assertIsOn() } + } + step("Click on '$networkTitle' switch") { + onManageTokensScreen { networkSwitch(networkTitle).performClick() } + } + step("Click 'Hide' button in dialog") { + onDialog { + dialogContainer.assertIsDisplayed() + hideButton.clickWithAssertion() + } + } + step("Assert switch is off") { + onManageTokensScreen { networkSwitch(networkTitle).assertIsOff() } + } + step("Click on 'Save' button") { + onManageTokensScreen { saveButton.performClick() } + } + step("Click on 'Account details' screen 'Back' button") { + waitForIdle() + onAccountDetails { topAppBarBackButton.performClick() } + } + step("Click on 'Wallet settings' screen 'Back' button") { + waitForIdle() + onWalletSettingsScreen { topAppBarBackButton.performClick() } + } + step("Click on 'Details' screen 'Back' button") { + waitForIdle() + onDetailsScreen { topAppBarBackButton.performClick() } + } + step("Assert token: '$tokenTitle' is not displayed") { + onMainScreen { assertTokenDoesNotExist(tokenTitle) } + } + } + } + + @AllureId("3626") + @DisplayName("Main: hide main coin via manage tokens") + @Test + fun hideMainCoinViaManageTokensTest() { + val tokenTitle = "POL (ex-MATIC)" + val networkTitle = "POLYGON" + val polygonTitle = "Polygon" + val accountName = "Main account" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open wallet details") { + waitForIdle() + onMainScreenTopBar { moreButton.clickWithAssertion() } + } + step("Open 'Wallet settings' screen") { + onDetailsScreen { walletNameButton.performClick() } + } + step("Click on account: '$accountName'") { + onWalletSettingsScreen { accountItem(accountName).performClick() } + } + step("Click on 'Manage tokens' button") { + onAccountDetails { manageTokensButton.performClick() } + } + step("Click on token: '$tokenTitle'") { + onManageTokensScreen { tokenItem(tokenTitle).performClick() } + } + step("Assert switch is on") { + onManageTokensScreen { networkSwitch(networkTitle).assertIsOn() } + } + step("Click on '$networkTitle' switch") { + onManageTokensScreen { networkSwitch(networkTitle).performClick() } + } + step("Click 'Hide' button in dialog") { + onDialog { + dialogContainer.assertIsDisplayed() + hideButton.clickWithAssertion() + } + } + step("Assert switch is off") { + onManageTokensScreen { networkSwitch(networkTitle).assertIsOff() } + } + step("Click on 'Save' button") { + onManageTokensScreen { saveButton.performClick() } + } + step("Click on 'Account details' screen 'Back' button") { + waitForIdle() + onAccountDetails { topAppBarBackButton.performClick() } + } + step("Click on 'Wallet settings' screen 'Back' button") { + waitForIdle() + onWalletSettingsScreen { topAppBarBackButton.performClick() } + } + step("Click on 'Details' screen 'Back' button") { + waitForIdle() + onDetailsScreen { topAppBarBackButton.performClick() } + } + step("Assert token: '$polygonTitle' is not displayed") { + onMainScreen { assertTokenDoesNotExist(polygonTitle) } + } + } + } + + @AllureId("3610") + @DisplayName("Main: check 'Unable to hide token' warning") + @Test + fun checkUnableToHideTokenWarningTest() { + val tokenTitle = "Ethereum" + val tokenSymbol = "ETH" + val scenarioState = "USDT" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Long click on token with name: '$tokenTitle'") { + waitForIdle() + onMainScreen { + tokenWithTitleAndAddress(tokenTitle).performTouchInput { + longClick( + position = center, + durationMillis = 1000L + ) + } + } + } + step("Click on 'Hide token' button") { + onTokenActionsBottomSheet { hideTokenButton.performClick() } + } + step("Assert 'Unable to hide $tokenTitle' alert title is displayed") { + onUnableToHideDialog { + unableToHideTokenTitle(tokenName = tokenTitle).assertIsDisplayed() + } + } + step("Assert 'Unable to hide $tokenTitle' alert message is displayed") { + onUnableToHideDialog { + unableToHideTokenMessage( + tokenName = tokenTitle, + tokenSymbol = tokenSymbol, + networkName = tokenTitle + ).assertIsDisplayed() + } + } + step("Click on 'Ok' button") { + onUnableToHideDialog { + okButton.performClick() + } + } + step("Press 'Back' button") { + device.uiDevice.pressBack() + } + step("Assert token: '$tokenTitle' is displayed") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).assertIsDisplayed() } + } + step("Click on token: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() } + } + step("Assert 'Token details screen' open") { + onTokenDetailsScreen { screenContainer.assertIsDisplayed() } + } + step("Click 'More' button") { + onTokenDetailsTopBar { moreButton.clickWithAssertion() } + } + step("Click 'Hide token' button") { + onPopUpMenu { + popUpContainer.assertIsDisplayed() + hideTokenButton.clickWithAssertion() + } + } + step("Assert 'Unable to hide $tokenSymbol' alert title is displayed") { + onUnableToHideDialog { + unableToHideTokenTitle(tokenName = tokenSymbol).assertIsDisplayed() + } + } + step("Assert 'Unable to hide $tokenTitle' alert message is displayed") { + onUnableToHideDialog { + unableToHideTokenMessage( + tokenName = tokenTitle, + tokenSymbol = tokenSymbol, + networkName = tokenTitle + ).assertIsDisplayed() + } + } + step("Click on 'Ok' button") { + onUnableToHideDialog { + okButton.performClick() + } + } + step("Click 'Back' button") { + onTokenDetailsTopBar { backButton.clickWithAssertion() } + } + step("Assert token: '$tokenTitle' is displayed") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).assertIsDisplayed() } + } + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/main/MainScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/main/MainScreenTest.kt new file mode 100644 index 0000000000..172bfbf08f --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/main/MainScreenTest.kt @@ -0,0 +1,114 @@ +package com.tangem.tests.main + +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.synchronizeAddresses +import com.tangem.screens.onMainScreen +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class MainScreenTest : BaseTestCase() { + + @AllureId("66") + @DisplayName("Main: check 'Organize tokens' button with multiple tokens no accounts") + @Test + fun checkOrganizeTokensButtonWithMultipleTokensNoAccountsTest() { + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Assert 'Organize tokens' button is displayed") { + onMainScreen { organizeTokensButton().assertIsDisplayed() } + } + } + } + + @AllureId("8748") + @DisplayName("Main: check 'Organize tokens' button with single token no accounts") + @Test + fun checkOrganizeTokensButtonWithSingleTokenNoAccountsTest() { + val scenarioState = "Cardano" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Assert 'Organize tokens' button is not displayed") { + onMainScreen { organizeTokensButtonNode.assertIsNotDisplayed()} + } + } + } + + @AllureId("8749") + @DisplayName("Main: check 'Organize tokens' button with single token two accounts") + @Test + fun checkOrganizeTokensButtonWithSingleTokenMultiAccountsTest() { + val scenarioState = "TwoAccountsSingleTokenEach" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Assert 'Organize tokens' button is not displayed") { + onMainScreen { organizeTokensButtonNode.assertIsNotDisplayed()} + } + } + } + + @AllureId("8750") + @DisplayName("Main: check 'Organize tokens' button with multiple tokens two accounts") + @Test + fun checkOrganizeTokensButtonWithMultipleTokensMultiAccountsTest() { + val scenarioState = "TwoAccountsMixed" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Assert 'Organize tokens' button is not displayed") { + onMainScreen { organizeTokensButtonNode.assertIsDisplayed()} + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/main/TokenListTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/main/TokenListTest.kt new file mode 100644 index 0000000000..b479c2aac6 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/main/TokenListTest.kt @@ -0,0 +1,51 @@ +package com.tangem.tests.main + +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.synchronizeAddresses +import com.tangem.screens.onMainScreen +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class TokenListTest : BaseTestCase() { + + @AllureId("180") + @DisplayName("Token list: hide token by long tap") + @Test + fun checkCustomDerivationIconOnTokenAndNetworkTest() { + val networkTitle = "Ethereum" + val customTokenTitle = "Myria" + val scenarioState = "CustomDerivation" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Assert token: '$networkTitle' is displayed") { + onMainScreen { tokenWithTitleAndAddress(networkTitle).assertIsDisplayed() } + } + step("Assert token with custom derivation icon: '$customTokenTitle' is displayed") { + onMainScreen { tokenWithCustomDerivationIcon(customTokenTitle).assertIsDisplayed() } + } + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/main/WarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/main/WarningsTest.kt new file mode 100644 index 0000000000..689da371e0 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/main/WarningsTest.kt @@ -0,0 +1,52 @@ +package com.tangem.tests.main + +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.synchronizeAddresses +import com.tangem.screens.onMainScreen +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class WarningsTest : BaseTestCase() { + + @AllureId("184") + @DisplayName("Token list: hide token by long tap") + @Test + fun checkUnavailableNetworksWarningTest() { + val scenarioState = "MissingDerivation" + val networkCount = 1 + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(USER_TOKENS_API_SCENARIO, scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses(isBalanceAvailable = false) + } + step("Assert 'Missing addresses' notification icon is displayed") { + onMainScreen { missingAddressNotificationIcon.assertIsDisplayed() } + } + step("Assert 'Missing addresses' notification title is displayed") { + onMainScreen { missingAddressNotificationTitle.assertIsDisplayed() } + } + step("Assert 'Missing addresses' notification message is displayed") { + onMainScreen { missingAddressNotificationMessage(networkCount).assertIsDisplayed() } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/amountScreen/SendAmountScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/amountScreen/SendAmountScreenTest.kt index dd6f386748..e521bde8a7 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/amountScreen/SendAmountScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/amountScreen/SendAmountScreenTest.kt @@ -23,7 +23,7 @@ class SendAmountScreenTest : BaseTestCase() { val manualSendAmount = "1" val clipboardSendAmount = "0.5" val invalidAmount = "2" - val errorText = getResourceString(R.string.send_validation_amount_exceeds_balance) + val errorText = getResourceString(R.string.common_insufficient_balance) val context = device.context setupHooks().run { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/StellarWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/StellarWarningsTest.kt index 3e7a6c9afd..fb4483da57 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/StellarWarningsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/StellarWarningsTest.kt @@ -17,7 +17,6 @@ import dagger.hilt.android.testing.HiltAndroidTest import io.github.kakaocup.kakao.common.utilities.getResourceString import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName -import org.junit.Ignore import org.junit.Test @HiltAndroidTest @@ -33,7 +32,6 @@ class StellarWarningsTest : BaseTestCase() { getResourceString(R.string.send_notification_invalid_reserve_amount_title, reserveAmount) private val warningMessage = getResourceString(R.string.send_notification_invalid_reserve_amount_text) - @Ignore("TODO: [REDACTED_JIRA]") @AllureId("4287") @DisplayName("Warnings: check warning, when sending less than reserve") @Test @@ -87,7 +85,6 @@ class StellarWarningsTest : BaseTestCase() { } } - @Ignore("TODO: [REDACTED_JIRA]") @AllureId("4286") @DisplayName("Warnings: check warning when sending amount equal to reserve") @Test @@ -142,7 +139,6 @@ class StellarWarningsTest : BaseTestCase() { } } - @Ignore("TODO: [REDACTED_JIRA]") @AllureId("4288") @DisplayName("Warnings: check warning when sending greater than reserve") @Test diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt index 4b35beb47b..a52fef59a6 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/UserWalletItem.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -43,6 +44,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.WalletSettingsScreenTestTags import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.StringsSigns.THREE_STARS @@ -64,7 +66,8 @@ fun UserWalletItem( modifier = Modifier .fillMaxWidth() .heightIn(min = TangemTheme.dimens.size68) - .padding(all = TangemTheme.dimens.spacing12), + .padding(all = TangemTheme.dimens.spacing12) + .testTag(WalletSettingsScreenTestTags.USER_ACCOUNT_ITEM), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt index de1f9aa454..1ddbb91962 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/TangemSwitch.kt @@ -3,10 +3,10 @@ package com.tangem.core.ui.components import androidx.compose.animation.animateColor import androidx.compose.animation.core.* import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ripple import androidx.compose.runtime.Composable @@ -20,7 +20,7 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.test.MarketsTestTags +import com.tangem.core.ui.test.SwitchTestTags @Suppress("MagicNumber") @Composable @@ -47,7 +47,8 @@ fun TangemSwitch( Box( modifier = modifier - .clickable( + .toggleable( + value = checked, interactionSource = interactionSource, indication = ripple( bounded = false, @@ -55,10 +56,9 @@ fun TangemSwitch( ), enabled = enabled, role = Role.Switch, - onClick = { - onCheckedChange(!checked) - }, - ).testTag(MarketsTestTags.ADD_TO_PORTFOLIO_SWITCH), + onValueChange = onCheckedChange, + ) + .testTag(SwitchTestTags.SWITCH), ) { BoxWithConstraints( modifier = Modifier 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 8439ed711d..9ec3a73b8d 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 @@ -13,12 +13,14 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.TokenElementsTestTags import com.tangem.core.ui.utils.getGreyScaleColorFilter /** @@ -147,7 +149,9 @@ private fun BoxScope.ContentIconContainer( if (icon.shouldShowCustomBadge) { CurrencyIconBottomBadge( - modifier = Modifier.align(Alignment.BottomEnd), + modifier = Modifier + .align(Alignment.BottomEnd) + .testTag(TokenElementsTestTags.TOKEN_CUSTOM_DERIVATION_ICON), ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt index 2c55637c6e..6b0577ad78 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/RowComponents.kt @@ -5,11 +5,13 @@ 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.testTag import androidx.compose.ui.text.style.TextOverflow import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.ManageTokensScreenTestTags @Composable inline fun RowContentContainer( @@ -27,18 +29,21 @@ inline fun RowContentContainer( Box( contentAlignment = Alignment.Center, content = icon, + modifier = Modifier.testTag(ManageTokensScreenTestTags.NETWORK_ICON), ) Box( modifier = Modifier .weight(1f) - .heightIn(min = TangemTheme.dimens.size22), + .heightIn(min = TangemTheme.dimens.size22) + .testTag(ManageTokensScreenTestTags.NETWORK_NAME), contentAlignment = Alignment.CenterStart, content = text, ) Box( modifier = Modifier .requiredWidthIn(max = TangemTheme.dimens.size80) - .heightIn(min = TangemTheme.dimens.size24), + .heightIn(min = TangemTheme.dimens.size24) + .testTag(ManageTokensScreenTestTags.SWITCH), contentAlignment = Alignment.CenterEnd, content = action, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/AccountDetailsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/AccountDetailsScreenTestTags.kt new file mode 100644 index 0000000000..ccc4793a3c --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/AccountDetailsScreenTestTags.kt @@ -0,0 +1,5 @@ +package com.tangem.core.ui.test + +object AccountDetailsScreenTestTags { + const val MANAGE_TOKENS_BUTTON = "ACCOUNT_DETAILS_SCREEN_MANAGE_TOKENS_BUTTON" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/ManageTokensScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/ManageTokensScreenTestTags.kt new file mode 100644 index 0000000000..d55ee2bb47 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/ManageTokensScreenTestTags.kt @@ -0,0 +1,8 @@ +package com.tangem.core.ui.test + +object ManageTokensScreenTestTags { + const val TOKEN_ITEM = "MANAGE_TOKENS_SCREEN_TOKEN_ITEM" + const val NETWORK_ICON = "MANAGE_TOKENS_SCREEN_NETWORK_ICON" + const val NETWORK_NAME = "MANAGE_TOKENS_SCREEN_NETWORK_NAME" + const val SWITCH = "MANAGE_TOKENS_SCREEN_SWITCH" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/MarketsTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/MarketsTestTags.kt index 8ab6a276ae..9569e50d89 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/MarketsTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/MarketsTestTags.kt @@ -3,6 +3,5 @@ package com.tangem.core.ui.test object MarketsTestTags { const val TOKENS_LIST = "MARKETS_TOKENS_LIST" const val TOKENS_LIST_ITEM = "MARKETS_TOKENS_LIST_ITEM" - const val ADD_TO_PORTFOLIO_SWITCH = "MARKETS_ADD_TO_PORTFOLIO_SWITCH" const val LISTED_ON_EXCHANGES_COUNT = "MARKETS_LISTED_ON_EXCHANGES_COUNT" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SwitchTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SwitchTestTags.kt new file mode 100644 index 0000000000..5684f179dd --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwitchTestTags.kt @@ -0,0 +1,5 @@ +package com.tangem.core.ui.test + +object SwitchTestTags { + const val SWITCH = "SWITCH" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/TokenElementsTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/TokenElementsTestTags.kt index 70277a9e2c..fc1a25782a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/TokenElementsTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/TokenElementsTestTags.kt @@ -9,4 +9,5 @@ object TokenElementsTestTags { const val TOKEN_CRYPTO_AMOUNT = "TOKEN_CRYPTO_AMOUNT" const val TOKEN_NON_FIAT_BLOCK = "TOKEN_NON_FIAT_BLOCK" const val TOKEN_YIELD_PROMO_BANNER = "TOKEN_YIELD_PROMO_BANNER" + const val TOKEN_CUSTOM_DERIVATION_ICON = "TOKEN_CUSTOM_DERIVATION_ICON" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/WalletSettingsScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/WalletSettingsScreenTestTags.kt index ade4f96e6f..6d178c0cd9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/WalletSettingsScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/WalletSettingsScreenTestTags.kt @@ -3,4 +3,5 @@ package com.tangem.core.ui.test object WalletSettingsScreenTestTags { const val SCREEN_CONTAINER = "WALLET_SETTINGS_SCREEN_CONTAINER" const val SCREEN_ITEM = "WALLET_SETTINGS_SCREEN_ITEM" + const val USER_ACCOUNT_ITEM = "WALLET_SETTINGS_USER_ACCOUNT_ITEM" } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt index 1ff4d5400f..b8a0613c61 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -31,6 +32,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.AccountDetailsScreenTestTags import com.tangem.features.account.details.entity.AccountDetailsUM @Composable @@ -130,7 +132,8 @@ private fun ManageTokensRow(state: AccountDetailsUM) { .clip(RoundedCornerShape(TangemTheme.dimens.radius12)) .background(TangemTheme.colors.background.primary) .clickable(onClick = state.onManageTokensClick) - .padding(all = TangemTheme.dimens.spacing12), + .padding(all = TangemTheme.dimens.spacing12) + .testTag(AccountDetailsScreenTestTags.MANAGE_TOKENS_BUTTON), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { 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 82579b41d9..1a2586e555 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 @@ -22,6 +22,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -53,6 +54,7 @@ import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.ManageTokensScreenTestTags import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection import com.tangem.domain.models.wallet.UserWalletId @@ -311,7 +313,9 @@ private fun BasicCurrencyItem(item: CurrencyItemUM.Basic, isEditable: Boolean, m Column(modifier = modifier) { ChainRow( - modifier = Modifier.clickable(onClick = item.onExpandClick), + modifier = Modifier + .clickable(onClick = item.onExpandClick) + .testTag(ManageTokensScreenTestTags.TOKEN_ITEM), model = with(item) { ChainRowUM( name = name, From bad7b92f6523d72a1064ff744b7ad4a55c5f1119 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 31 Mar 2026 18:26:19 +0400 Subject: [PATCH 45/75] Updated on 2026-08-14 --- .../core/analytics/models/AnalyticsParam.kt | 1 + .../domain/swap/models/SwapCurrencies.kt | 15 +++- .../v2/impl/amount/model/SwapAmountModel.kt | 31 ++++++++ ...wapAmountSecondaryReadyStateTransformer.kt | 11 +-- .../DefaultSendWithSwapComponent.kt | 45 ++++------- .../analytics/SendWithSwapAnalyticEvents.kt | 75 +++++++++++++++++++ .../confirm/model/SendWithSwapConfirmModel.kt | 13 ++++ 7 files changed, 154 insertions(+), 37 deletions(-) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 946ad653c4..a670a8226a 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -295,6 +295,7 @@ sealed class AnalyticsParam { const val REFERRAL = "Referral" const val REFERRAL_ID = "Referral_ID" const val SEARCHED = "Searched" + const val RATE_TYPE = "Rate Type" } } diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt index b22f150071..4eee1933a8 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapCurrencies.kt @@ -1,6 +1,7 @@ package com.tangem.domain.swap.models import com.tangem.domain.express.models.ExpressProvider +import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrencyStatus /** @@ -50,4 +51,16 @@ data class SwapCurrenciesGroup( data class SwapCryptoCurrency( val currencyStatus: CryptoCurrencyStatus, val providers: List, -) \ No newline at end of file +) + +/** + * Get initial rate type based on available providers + */ +fun List.getInitialRateType(): ExpressRateType { + val availableRateTypes = this.flatMap { it.rateTypes }.toSet() + return if (availableRateTypes.contains(ExpressRateType.Fixed)) { + ExpressRateType.Fixed + } else { + ExpressRateType.Float + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 90eea6d0fa..b17f11cbb6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -53,6 +53,8 @@ import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderCompone import com.tangem.features.swap.v2.impl.common.SwapAlertFactory import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.sendviaswap.SendWithSwapRoute +import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents +import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents.NoticeFixedRate.toAnalyticsRateType import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import com.tangem.utils.coroutines.PeriodicTask @@ -117,6 +119,7 @@ internal class SwapAmountModel @Inject constructor( val rateInfoNavigation: SlotNavigation = SlotNavigation() private var isShowBestRateAnimation: Boolean = false + private var lastAmountScreenOpenedCurrencyId: CryptoCurrency.ID? = null private var autoUpdateSubscriberJob: Job? = null @@ -328,6 +331,11 @@ internal class SwapAmountModel @Inject constructor( override fun onRateClick() { val content = uiState.value as? SwapAmountUM.Content ?: return rateInfoNavigation.activate(content.swapRateType) + val event = when (content.swapRateType) { + ExpressRateType.Float -> SendWithSwapAnalyticEvents.NoticeFloatRate + ExpressRateType.Fixed -> SendWithSwapAnalyticEvents.NoticeFixedRate + } + analyticsEventHandler.send(event) } override fun onSeparatorClick() { @@ -593,6 +601,7 @@ internal class SwapAmountModel @Inject constructor( if (currentState != null && currentState.swapRateMode != SwapRateMode.FLOAT_ONLY) { computeAndSetSecondaryAmount(currentState) } + sendAmountScreenOpenedIfNeeded(secondaryStatus) startLoadingQuotesTask(isSilentReload = false) } else { @Suppress("NullableToStringCall") @@ -603,11 +612,33 @@ internal class SwapAmountModel @Inject constructor( | Secondary -> $secondaryStatus """.trimIndent(), ) + analyticsEventHandler.send( + SendWithSwapAnalyticEvents.SendWithSwapError( + errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Amount, + message = "Invalid cryptocurrencies status: primary=$primaryStatus, secondary=$secondaryStatus", + ), + ) showErrorAlert(errorMessage = null) } } } + private fun sendAmountScreenOpenedIfNeeded(secondaryStatus: CryptoCurrencyStatus) { + val currencyId = secondaryStatus.currency.id + if (lastAmountScreenOpenedCurrencyId == currencyId) return + lastAmountScreenOpenedCurrencyId = currencyId + + val content = uiState.value as? SwapAmountUM.Content ?: return + + analyticsEventHandler.send( + SendWithSwapAnalyticEvents.AmountScreenOpened( + rateType = content.swapRateType.toAnalyticsRateType(), + fromToken = content.primaryCryptoCurrencyStatus.currency, + toToken = secondaryStatus.currency, + ), + ) + } + private suspend fun initCurrencies(primaryStatus: CryptoCurrencyStatus, secondaryStatus: CryptoCurrencyStatus?) { primaryMinimumAmountBoundary = EnterAmountBoundary( amount = getMinimumTransactionAmountSyncUseCase diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt index 2fb41d9e71..f9de0f0a59 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt @@ -11,6 +11,7 @@ import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapRateMode +import com.tangem.domain.swap.models.getInitialRateType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountFieldConverter import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM @@ -52,11 +53,7 @@ internal class SwapAmountSecondaryReadyStateTransformer( rateTypes.contains(ExpressRateType.Fixed) -> SwapRateMode.FIXED_ONLY else -> SwapRateMode.FLOAT_ONLY } - val selectedAmountType = if (swapRateMode != SwapRateMode.FLOAT_ONLY) { - ExpressRateType.Fixed - } else { - ExpressRateType.Float - } + val selectedRateType = providers.getInitialRateType() return SwapAmountUM.Content( isPrimaryButtonEnabled = false, primaryAmount = prevState.primaryAmount, @@ -68,13 +65,13 @@ internal class SwapAmountSecondaryReadyStateTransformer( ), secondaryCryptoCurrencyStatus = secondaryCryptoCurrencyStatus, swapCurrencies = swapCurrencies, - selectedAmountType = if (selectedAmountType == ExpressRateType.Fixed) { + selectedAmountType = if (selectedRateType == ExpressRateType.Fixed) { SwapAmountType.To } else { SwapAmountType.From }, swapDirection = swapDirection, - swapRateType = selectedAmountType, + swapRateType = selectedRateType, swapQuotes = persistentListOf(), selectedQuote = SwapQuoteUM.Empty, appCurrency = appCurrency, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt index b6d57e6d09..9adf52cf8f 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt @@ -20,11 +20,9 @@ import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.models.account.Account import com.tangem.domain.swap.models.R import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents -import com.tangem.features.send.v2.api.entry.SendEntryRoute import com.tangem.features.send.v2.api.subcomponents.destination.DestinationRoute import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponentParams @@ -34,6 +32,8 @@ import com.tangem.features.swap.v2.impl.amount.SwapAmountComponentParams import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.common.SwapUtils.SEND_WITH_SWAP_PROVIDER_TYPES import com.tangem.features.swap.v2.impl.common.entity.ConfirmUM +import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents +import com.tangem.features.swap.v2.impl.sendviaswap.analytics.SendWithSwapAnalyticEvents.NoticeFixedRate.toAnalyticsRateType import com.tangem.features.swap.v2.impl.sendviaswap.confirm.SendWithSwapConfirmComponent import com.tangem.features.swap.v2.impl.sendviaswap.model.SendWithSwapModel import com.tangem.features.swap.v2.impl.sendviaswap.success.SendWithSwapSuccessComponent @@ -87,17 +87,6 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( componentScope.launch { when (val activeComponent = stack.active.instance) { is SwapAmountComponent -> { - if ( - params.currentRoute.value is SendEntryRoute.SendWithSwap && - model.currentRoute.value != stack.active.configuration - ) { - analyticsEventHandler.send( - CommonSendAnalyticEvents.AmountScreenOpened( - categoryName = model.analyticCategoryName, - source = model.analyticsSendSource, - ), - ) - } activeComponent.updateState(model.uiState.value.amountUM) } is SendDestinationComponent -> { @@ -110,25 +99,23 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( activeComponent.updateState(model.uiState.value.destinationUM) } is SendWithSwapConfirmComponent -> { - val fromCurrency = params.currency - val fromDerivationIndex = when (val account = model.accountFlow.value) { - is Account.CryptoPortfolio -> account.derivationIndex.value - is Account.Payment -> TODO("[REDACTED_JIRA]") - null -> null - }.takeIf { model.isAccountModeFlow.value } - analyticsEventHandler.send( - CommonSendAnalyticEvents.ConfirmationScreenOpened( - categoryName = model.analyticCategoryName, - source = model.analyticsSendSource, - sendBlockchain = fromCurrency.network.name, - sendToken = fromCurrency.symbol, - fromDerivationIndex = fromDerivationIndex, - toDerivationIndex = null, - ), - ) if (model.currentRoute.value.isEditMode) { activeComponent.updateState(model.uiState.value) } + val fromCurrency = params.currency + val content = model.uiState.value.amountUM as? SwapAmountUM.Content ?: return@launch + val toCurrency = content.secondaryCryptoCurrencyStatus?.currency ?: return@launch + val rateType = content.swapRateType.toAnalyticsRateType() + val providerName = content.selectedQuote.provider?.name.orEmpty() + + analyticsEventHandler.send( + SendWithSwapAnalyticEvents.ConfirmationScreenOpened( + providerName = providerName, + rateType = rateType, + fromToken = fromCurrency, + toToken = toCurrency, + ), + ) } } model.currentRoute.emit(stack.active.configuration) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt index 24a68d2945..c0d35dcc2d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt @@ -4,13 +4,16 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FROM import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_TO +import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_TOKEN import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_TOKEN +import com.tangem.core.analytics.models.AnalyticsParam.Key.RATE_TYPE import com.tangem.core.analytics.models.AppsFlyerIncludedEvent +import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents @@ -19,6 +22,40 @@ internal sealed class SendWithSwapAnalyticEvents( params: Map = emptyMap(), ) : AnalyticsEvent(category = CommonSendAnalyticEvents.SEND_CATEGORY, event = event, params = params) { + /** Confirmation screen opened */ + data class ConfirmationScreenOpened( + val providerName: String, + val rateType: RateType, + val fromToken: CryptoCurrency, + val toToken: CryptoCurrency, + ) : SendWithSwapAnalyticEvents( + event = "Send With Swap Confirm Screen Opened", + params = buildMap { + put(SEND_TOKEN, fromToken.symbol) + put(RECEIVE_TOKEN, toToken.symbol) + put(SEND_BLOCKCHAIN, fromToken.network.name) + put(RECEIVE_BLOCKCHAIN, toToken.network.name) + put(RATE_TYPE, rateType.name) + put(PROVIDER, providerName) + }, + ), AppsFlyerIncludedEvent + + /** Amount screen opened */ + data class AmountScreenOpened( + val rateType: RateType, + val fromToken: CryptoCurrency, + val toToken: CryptoCurrency, + ) : SendWithSwapAnalyticEvents( + event = "Send With Swap Amount Screen Opened", + params = buildMap { + put(SEND_TOKEN, fromToken.symbol) + put(RECEIVE_TOKEN, toToken.symbol) + put(SEND_BLOCKCHAIN, fromToken.network.name) + put(RECEIVE_BLOCKCHAIN, toToken.network.name) + put(RATE_TYPE, rateType.name) + }, + ), AppsFlyerIncludedEvent + data class TransactionScreenOpened( val providerName: String, val feeType: AnalyticsParam.FeeType, @@ -72,4 +109,42 @@ internal sealed class SendWithSwapAnalyticEvents( SEND_BLOCKCHAIN to fromToken.network.name, ), ) + + data object NoticeFixedRate : SendWithSwapAnalyticEvents( + event = "Notice - Fixed Rate", + params = emptyMap(), + ) + + data object NoticeFloatRate : SendWithSwapAnalyticEvents( + event = "Notice - Float Rate", + params = emptyMap(), + ) + + data class SendWithSwapError( + val errorScreen: ErrorScreen, + val message: String, + ) : SendWithSwapAnalyticEvents( + event = when (errorScreen) { + ErrorScreen.Amount -> "Send With Swap Amount Screen Error" + ErrorScreen.Confirm -> "Send With Swap Confirm Screen Error" + }, + params = mapOf( + ERROR_MESSAGE to message, + ), + ) + + enum class ErrorScreen { + Amount, + Confirm, + } + + enum class RateType { + Float, + Fixed, + } + + fun ExpressRateType.toAnalyticsRateType(): RateType = when (this) { + ExpressRateType.Float -> RateType.Float + ExpressRateType.Fixed -> RateType.Fixed + } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 34b87c65bb..2359164b49 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -310,6 +310,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( } } + @Suppress("LongMethod") private fun onSendClick() { val provider = confirmData.quote?.provider ?: return modelScope.launch { @@ -322,6 +323,12 @@ internal class SendWithSwapConfirmModel @Inject constructor( isAmountSubtractAvailable = isAmountSubtractAvailable, onExpressError = { expressError -> uiState.transformerUpdate(SendWithSwapConfirmSendingStateTransformer(false)) + analyticsEventHandler.send( + SendWithSwapAnalyticEvents.SendWithSwapError( + errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Confirm, + message = "Express error: $expressError", + ), + ) swapAlertFactory.getGenericErrorState( expressError = expressError, onFailedTxEmailClick = { @@ -339,6 +346,12 @@ internal class SendWithSwapConfirmModel @Inject constructor( }, onSendError = { error -> uiState.transformerUpdate(SendWithSwapConfirmSendingStateTransformer(false)) + analyticsEventHandler.send( + SendWithSwapAnalyticEvents.SendWithSwapError( + errorScreen = SendWithSwapAnalyticEvents.ErrorScreen.Confirm, + message = "Send error: ${error?.toString().orEmpty()}", + ), + ) swapAlertFactory.getSendTransactionErrorState( error = error, onFailedTxEmailClick = { _ -> From 1f617808db3d9d3b14a87514f2114aa54a710b13 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 31 Mar 2026 22:05:23 +0500 Subject: [PATCH 46/75] Updated on 2026-08-14 --- .../impl/model/ChooseTokenModel.kt | 17 +- .../converters/AccountTokenItemConverter.kt | 23 +- .../swap/converters/TokensDataConverter.kt | 10 +- .../feature/swap/ui/SwapSelectTokenScreen.kt | 208 ++++++++++++------ .../preview/SwapSelectTokenPreviewProvider.kt | 160 ++++++++++---- 5 files changed, 302 insertions(+), 116 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt index 9348c16c67..7d7812024f 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt @@ -16,6 +16,7 @@ import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketListConfig import com.tangem.domain.markets.toSerializableParam +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetWalletsUseCase @@ -62,6 +63,8 @@ internal class ChooseTokenModel @Inject constructor( private val visibleMarketItemIds = MutableStateFlow>(emptyList()) private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) + private val expandedAccountsFlow: MutableStateFlow> = MutableStateFlow(emptyMap()) + var addToPortfolioManager: AddToPortfolioManager? = null val addToPortfolioCallback = object : AddToPortfolioComponent.Callback { override fun onDismiss() = bottomSheetNavigation.dismiss() @@ -104,17 +107,27 @@ internal class ChooseTokenModel @Inject constructor( flow = bridge.currenciesGroup, flow2 = settingContextUseCase.invoke(), flow3 = marketsStateFlow(), - transform = { currenciesGroup, settingContext, marketState -> + flow4 = expandedAccountsFlow, + transform = { currenciesGroup, settingContext, marketState, expandedAccounts -> val isAccountsMode = settingContext.isAccountsMode val appCurrency = settingContext.appCurrency val isBalanceHidden = settingContext.isBalanceHidden + TokensDataConverter( onSearchEntered = { query -> bridge.onSearchQuery(query) }, - onTokenSelected = { tokenId -> + onTokenClick = { tokenId -> val selected = tokenId to ChooseTokenAnalyticsPayload .IsSearched(searchQueryState.value.isNotEmpty()) bridge.onTokenSelected(selected) }, + onAccountClick = { account -> + expandedAccountsFlow.update { expandedList -> + val hasSavedAccount = expandedList[account.accountId] + val isExpanded = hasSavedAccount == true + expandedList + (account.accountId to !isExpanded) + } + }, + expandedAccounts = expandedAccounts, tokensDataState = currenciesGroup, isBalanceHidden = isBalanceHidden, isAccountsMode = isAccountsMode, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt index 915211edc6..b28d71800a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt @@ -15,16 +15,22 @@ 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.models.StatusSource import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.ui.AccountSwapAvailability import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.toPersistentList internal class AccountTokenItemConverter( private val appCurrency: AppCurrency, private val unavailableErrorText: TextReference, - private val onItemClick: (String) -> Unit, + private val expandedAccounts: Map, + private val onTokenItemClick: (String) -> Unit, + private val onAccountItemClick: (Account.CryptoPortfolio) -> Unit, ) : Converter { override fun convert(value: AccountSwapAvailability): TokensListItemUM.Portfolio { @@ -32,10 +38,15 @@ internal class AccountTokenItemConverter( tokenItemUM = AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, account = value.account, - onItemClick = null, - ).convert(TotalFiatBalance.Failed), - isExpanded = true, - isCollapsable = false, + onItemClick = onAccountItemClick, + ).convert( + TotalFiatBalance.Loaded( + amount = value.currencyList.sumOf { it.cryptoCurrencyStatus.value.fiatAmount.orZero() }, + source = StatusSource.ONLY_CACHE, + ), + ), + isExpanded = expandedAccounts[value.account.accountId] != false, + isCollapsable = true, tokens = value.currencyList.map { accountSwapCurrency -> createAvailableItemConverter() .convert(accountSwapCurrency.cryptoCurrencyStatus) @@ -57,7 +68,7 @@ internal class AccountTokenItemConverter( fiatAmountStateProvider = { createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = true) }, - onItemClick = { account, currencyStatus -> onItemClick(currencyStatus.currency.id.value) }, + onItemClick = { account, currencyStatus -> onTokenItemClick(currencyStatus.currency.id.value) }, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index 0b7cb9c397..64ba7cf5b0 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -3,6 +3,8 @@ package com.tangem.feature.swap.converters import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenListUMData @@ -15,7 +17,9 @@ import kotlinx.collections.immutable.toPersistentList @Suppress("LongParameterList") internal class TokensDataConverter( private val onSearchEntered: (String) -> Unit, - private val onTokenSelected: (String) -> Unit, + onTokenClick: (String) -> Unit, + onAccountClick: (Account.CryptoPortfolio) -> Unit, + private val expandedAccounts: Map, private val tokensDataState: CurrenciesGroup, private val isBalanceHidden: Boolean, private val isAccountsMode: Boolean, @@ -26,7 +30,9 @@ internal class TokensDataConverter( private val accountListItemConverter = AccountTokenItemConverter( appCurrency = appCurrency, unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), - onItemClick = onTokenSelected, + onTokenItemClick = onTokenClick, + onAccountItemClick = onAccountClick, + expandedAccounts = expandedAccounts, ) fun transform(): SwapSelectTokenStateHolder { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index a117b2df9c..6758ca18b7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -1,21 +1,16 @@ package com.tangem.feature.swap.ui +import android.content.res.Configuration import androidx.activity.compose.BackHandler +import androidx.compose.animation.* +import androidx.compose.animation.core.* import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.lazy.* import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter @@ -30,6 +25,7 @@ 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.SpacerH import com.tangem.core.ui.components.appbar.ExpandableSearchView import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.components.tokenlist.PortfolioListItem @@ -37,10 +33,11 @@ import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem import com.tangem.core.ui.components.tokenlist.TokenListItem import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.BuyTokenScreenTestTags +import com.tangem.core.ui.test.MainScreenTestTags import com.tangem.core.ui.utils.lazyListItemPosition import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenListUMData @@ -182,6 +179,10 @@ private fun ListOfTokensWithMarkets( isBalanceHidden = state.isBalanceHidden, ) + item("spacer_before_markets") { + SpacerH(32.dp) + } + swapMarketsListItems(marketsState) } @@ -248,10 +249,12 @@ private fun LazyListScope.assetsTitle(count: Int, showCount: Boolean) { private fun LazyListScope.tokensListItems(tokensListData: TokenListUMData, isBalanceHidden: Boolean) { when (tokensListData) { is TokenListUMData.AccountList -> { - tokensListData.tokensList.forEach { item -> + tokensListData.tokensList.forEachIndexed { index, item -> portfolioTokensList( portfolio = item, isBalanceHidden = isBalanceHidden, + portfolioIndex = index, + modifier = Modifier, ) } } @@ -287,97 +290,168 @@ private fun LazyListScope.tokensList(items: ImmutableList, isB ) } -internal fun LazyListScope.portfolioTokensList(portfolio: TokensListItemUM.Portfolio, isBalanceHidden: Boolean) { +internal fun LazyListScope.portfolioTokensList( + portfolio: TokensListItemUM.Portfolio, + modifier: Modifier, + portfolioIndex: Int, + isBalanceHidden: Boolean, +) { val tokens = portfolio.tokens val isExpanded = portfolio.isExpanded + val lastIndex = tokens.lastIndex.inc() portfolioItem( portfolio = portfolio, - modifier = Modifier, + modifier = modifier, + portfolioIndex = portfolioIndex, isBalanceHidden = isBalanceHidden, ) - if (!isExpanded) return itemsIndexed( items = tokens, - key = { _, item -> item.id }, + key = { _, item -> item.id.toString() + "-portfolio-${portfolio.id}" }, contentType = { _, item -> item::class.java }, itemContent = { tokenIndex, token -> val indexWithHeader = tokenIndex.inc() - PortfolioTokensListItem( - state = token, - isBalanceHidden = isBalanceHidden, - modifier = Modifier - .animateItem() + SlideInItemVisibility( + currentIndex = tokenIndex, + lastIndex = lastIndex, + modifier = modifier + .testModifier(indexWithHeader) + .animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null) .roundedShapeItemDecoration( + radius = TangemTheme.dimens.radius14, currentIndex = indexWithHeader, - lastIndex = tokens.lastIndex.inc(), + lastIndex = lastIndex, backgroundColor = TangemTheme.colors.background.primary, - ) - .conditional(tokenIndex == tokens.lastIndex) { - Modifier.padding(bottom = 8.dp) - }, - ) + ), + visible = isExpanded, + ) { + val innerModifier = if (indexWithHeader == lastIndex) Modifier.padding(bottom = 8.dp) else Modifier + PortfolioTokensListItem( + state = token, + isBalanceHidden = isBalanceHidden, + modifier = innerModifier, + ) + } }, ) } +@Suppress("MagicNumber") private fun LazyListScope.portfolioItem( portfolio: TokensListItemUM.Portfolio, modifier: Modifier, + portfolioIndex: Int, isBalanceHidden: Boolean, ) { + val tokens = portfolio.tokens + val isExpanded = portfolio.isExpanded + val lastIndex = when { + isExpanded && tokens.isEmpty() -> 1 + isExpanded -> tokens.lastIndex.inc() + else -> 0 + } + item( key = "account-${portfolio.id}", - contentType = "account", + contentType = "account-content", ) { + // Snap immediately on expand; on collapse, hold until all child items finish + // their shrink animation, then snap to fully-rounded shape. + val effectiveLastIndex by animateIntAsState( + targetValue = lastIndex, + animationSpec = if (lastIndex != 0) { + snap() + } else { + snap(delayMillis = minOf(50 * tokens.lastIndex, 250) + 150) + }, + label = "lastIndex", + ) + PortfolioListItem( state = portfolio, isBalanceHidden = isBalanceHidden, - modifier = Modifier - .animateItem() + modifier = modifier + .testModifier(portfolioIndex) .roundedShapeItemDecoration( currentIndex = 0, - lastIndex = portfolio.tokens.lastIndex.inc(), + radius = TangemTheme.dimens.radius14, + lastIndex = effectiveLastIndex, backgroundColor = TangemTheme.colors.background.primary, - ) - .then(modifier), + ), + ) + } +} + +private fun Modifier.testModifier(index: Int): Modifier = this + .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) + .semantics { lazyListItemPosition = index } + +@Suppress("MagicNumber") +@Composable +internal fun SlideInItemVisibility( + visible: Boolean, + currentIndex: Int, + lastIndex: Int, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + val maxDelay = 250 + val delayEnter = minOf(50 * currentIndex, maxDelay) + val delayExit = minOf(50 * (lastIndex - currentIndex - 1), maxDelay) + + AnimatedVisibility( + modifier = modifier, + visible = visible, + enter = expandVertically( + tween(200, delayMillis = delayEnter, easing = LinearOutSlowInEasing), + expandFrom = Alignment.Top, + ) + fadeIn(tween(200, delayMillis = delayEnter, easing = LinearOutSlowInEasing)), + exit = shrinkVertically( + tween(150, delayMillis = delayExit, easing = FastOutLinearInEasing), + shrinkTowards = Alignment.Top, + ) + fadeOut(tween(150, delayMillis = delayExit, easing = FastOutLinearInEasing)), + ) { + content() + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun SwapSelectTokenScreen_Preview( + @PreviewParameter(SwapSelectTokenScreenPreviewProvider::class) params: SwapSelectTokenStateHolder, +) { + TangemThemePreview { + SwapSelectTokenScreen( + state = params, + onBack = {}, ) } } private class SwapSelectTokenScreenPreviewProvider : PreviewParameterProvider { - override val values: Sequence = sequenceOf( - // Content state with tokens and markets - SwapSelectTokenPreviewProvider().provideSwapSelectTokenState(), - // Empty state - SwapSelectTokenStateHolder( - tokensListData = TokenListUMData.EmptyList, - marketsState = SwapMarketState.DefaultLoading, - isAfterSearch = false, - isBalanceHidden = false, - onSearchEntered = {}, - ), - // Not found state - SwapSelectTokenStateHolder( - tokensListData = TokenListUMData.EmptyList, - marketsState = SwapMarketState.SearchLoading, - isAfterSearch = true, - isBalanceHidden = false, - onSearchEntered = {}, - ), - ) -} - -@Preview -@Composable -private fun TokenScreenPreview( - @PreviewParameter(SwapSelectTokenScreenPreviewProvider::class) - state: SwapSelectTokenStateHolder, -) { - TangemThemePreview { - SwapSelectTokenScreen( - state = state, - onBack = {}, + override val values: Sequence + get() = sequenceOf( + // Content state with tokens and markets + SwapSelectTokenPreviewProvider.defaultState, + // Empty state + SwapSelectTokenStateHolder( + tokensListData = TokenListUMData.EmptyList, + marketsState = SwapMarketState.DefaultLoading, + isAfterSearch = false, + isBalanceHidden = false, + onSearchEntered = {}, + ), + // Not found state + SwapSelectTokenStateHolder( + tokensListData = TokenListUMData.EmptyList, + marketsState = SwapMarketState.SearchLoading, + isAfterSearch = true, + isBalanceHidden = false, + onSearchEntered = {}, + ), ) - } -} \ No newline at end of file +} +// endregion \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt index 632255291e..f61c2e0da4 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt @@ -2,30 +2,107 @@ package com.tangem.feature.swap.ui.preview import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.ui.R +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.token.AccountItemPreviewData +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM +import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.core.ui.R import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenListUMData import com.tangem.feature.swap.models.market.state.SwapMarketState -import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList -internal class SwapSelectTokenPreviewProvider { +internal object SwapSelectTokenPreviewProvider { - fun provideSwapSelectTokenState(): SwapSelectTokenStateHolder { - return SwapSelectTokenStateHolder( - tokensListData = TokenListUMData.EmptyList, - isAfterSearch = false, - isBalanceHidden = false, - onSearchEntered = {}, - marketsState = createPreviewMarketsState(), - ) - } + private const val CHART_VALUE_1 = 0.4 + private const val CHART_VALUE_2 = 0.2 + private const val CHART_VALUE_3 = 0.1 + private const val CHART_VALUE_4 = 2.0 + private const val CHART_VALUE_5 = 5.0 + private const val CHART_VALUE_6 = 3.0 + private const val TOTAL_ITEMS = 322 - private fun createPreviewMarketsState() = SwapMarketState.Content( + private val PREVIEW_CHART_DATA = MarketChartRawData( + y = persistentListOf( + CHART_VALUE_1, + CHART_VALUE_2, + CHART_VALUE_1, + CHART_VALUE_3, + CHART_VALUE_1, + CHART_VALUE_4, + CHART_VALUE_5, + CHART_VALUE_3, + CHART_VALUE_4, + CHART_VALUE_4, + CHART_VALUE_6, + ), + ) + + private val tokenItemState = TokenItemState.Content( + id = "1", + iconState = CurrencyIconState.Locked, + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "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 = {}, + ) + + private val textContentTokensState = persistentListOf( + TokensListItemUM.GroupTitle(id = 111, text = stringReference("Network Bitcoin")), + TokensListItemUM.Token(state = tokenItemState), + TokensListItemUM.GroupTitle(id = 222, text = stringReference("Network Ethereum")), + TokensListItemUM.Token( + state = tokenItemState.copy( + id = "2", + titleState = TokenItemState.TitleState.Content(text = stringReference("Ethereum")), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "3 340,79 \$"), + subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "1,856660295 ETH"), + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( + price = "1 799,41 \$", + priceChangePercent = "5,16 %", + type = PriceChangeType.UP, + ), + ), + ), + TokensListItemUM.Token( + state = TokenItemState.Unreachable( + id = "3", + iconState = CurrencyIconState.Locked, + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")), + onItemClick = {}, + onItemLongClick = {}, + ), + ), + TokensListItemUM.Token( + state = tokenItemState.copy( + id = "4", + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Shiba Inu")), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "48,64 \$"), + subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "6 200 220,00 SHIB"), + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( + price = "0.01 \$", + priceChangePercent = "1,34 %", + type = PriceChangeType.DOWN, + ), + ), + ), + ) + + private val marketState = SwapMarketState.Content( items = createPreviewMarketItems(), loadMore = { }, onItemClick = { }, @@ -35,6 +112,37 @@ internal class SwapSelectTokenPreviewProvider { shouldAssetsCount = false, ) + val defaultState = SwapSelectTokenStateHolder( + tokensListData = TokenListUMData.AccountList( + tokensList = persistentListOf( + TokensListItemUM.Portfolio( + content = PortfolioItemContentUM.Tokens( + tokens = textContentTokensState.filterIsInstance() + .toPersistentList(), + ), + isExpanded = false, + isCollapsable = true, + tokenItemUM = AccountItemPreviewData.accountItem + .copy(iconState = AccountItemPreviewData.accountLetterIcon), + ), + TokensListItemUM.Portfolio( + content = PortfolioItemContentUM.Tokens( + tokens = textContentTokensState.filterIsInstance() + .toPersistentList(), + ), + isExpanded = true, + isCollapsable = true, + tokenItemUM = AccountItemPreviewData.accountItem, + ), + ), + totalTokensCount = TOTAL_ITEMS, + ), + isAfterSearch = false, + isBalanceHidden = false, + onSearchEntered = {}, + marketsState = marketState, + ) + private fun createPreviewMarketItems() = listOf( createMarketItem( id = "1", @@ -105,30 +213,4 @@ internal class SwapSelectTokenPreviewProvider { stakingRate = stringReference("APY 12.34%"), updateTimestamp = 0, ) - - companion object { - private const val CHART_VALUE_1 = 0.4 - private const val CHART_VALUE_2 = 0.2 - private const val CHART_VALUE_3 = 0.1 - private const val CHART_VALUE_4 = 2.0 - private const val CHART_VALUE_5 = 5.0 - private const val CHART_VALUE_6 = 3.0 - private const val TOTAL_ITEMS = 322 - - private val PREVIEW_CHART_DATA = MarketChartRawData( - y = persistentListOf( - CHART_VALUE_1, - CHART_VALUE_2, - CHART_VALUE_1, - CHART_VALUE_3, - CHART_VALUE_1, - CHART_VALUE_4, - CHART_VALUE_5, - CHART_VALUE_3, - CHART_VALUE_4, - CHART_VALUE_4, - CHART_VALUE_6, - ), - ) - } } \ No newline at end of file From 4ff20f347944efdb23ff5167ad87e3e8016a6d8c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 31 Mar 2026 20:05:34 +0300 Subject: [PATCH 47/75] Updated on 2026-08-14 --- .../confirm/model/NFTSendConfirmModel.kt | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index b0a07a0db9..b15638b859 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -16,8 +16,11 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.wallet.isHotWallet import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase @@ -88,6 +91,7 @@ internal class NFTSendConfirmModel @Inject constructor( private val nftSendAnalyticHelper: NFTSendAnalyticHelper, private val nftSendSuccessTrigger: NFTSendSuccessTrigger, private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, + private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles, sendBalanceUpdaterFactory: SendBalanceUpdater.Factory, ) : Model(), NFTSendConfirmClickIntents, SendNotificationsComponent.ModelCallback, FeeSelectorModelCallback { @@ -419,21 +423,17 @@ internal class NFTSendConfirmModel @Inject constructor( private fun primaryButtonUM(): NavigationButton { val confirmUM = uiState.value.confirmUM - val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isSending + val isContent = confirmUM is ConfirmUM.Content + val isReadyToSend = isContent && !confirmUM.isSending + val isHoldToConfirm = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled && + userWallet.isHotWallet && isContent return NavigationButton( - textReference = when (confirmUM) { - is ConfirmUM.Success -> resourceReference(R.string.common_close) - is ConfirmUM.Content -> if (confirmUM.isSending) { - resourceReference(R.string.send_sending) - } else { - resourceReference(R.string.common_send) - } - else -> resourceReference(R.string.common_send) - }, + textReference = getPrimaryButtonText(confirmUM, isHoldToConfirm), iconRes = walletInterationIcon(userWallet), - isIconVisible = isReadyToSend, + isIconVisible = isReadyToSend && !isHoldToConfirm, isEnabled = confirmUM.isPrimaryButtonEnabled, isHapticClick = isReadyToSend, + isHoldToConfirm = isHoldToConfirm, onClick = { when (confirmUM) { is ConfirmUM.Success -> { @@ -453,6 +453,15 @@ internal class NFTSendConfirmModel @Inject constructor( ) } + private fun getPrimaryButtonText(confirmUM: ConfirmUM, isHoldToConfirm: Boolean): TextReference { + return when { + isHoldToConfirm -> resourceReference(R.string.common_send) + confirmUM is ConfirmUM.Success -> resourceReference(R.string.common_close) + confirmUM is ConfirmUM.Content && confirmUM.isSending -> resourceReference(R.string.send_sending) + else -> resourceReference(R.string.common_send) + } + } + private companion object { const val CHECK_FEE_UPDATE_DELAY = 10_000L } From 0c5ff2eb37b9f2b6dd82f9d9b149006aea452cda Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 1 Apr 2026 08:16:56 +0200 Subject: [PATCH 48/75] Updated on 2026-08-14 --- .../core/ui/ds/button/TangemButtonInternal.kt | 2 +- .../components/DefaultFeedEntryComponent.kt | 16 -- .../feed/components/FeedEntryChildFactory.kt | 12 ++ .../list/DefaultMarketsTokenListComponent.kt | 75 +++++++- .../model/market/list/MarketsListModel.kt | 11 +- .../model/market/list/state/MarketsListUM.kt | 3 + .../model/market/list/state/SortByMenuUM.kt | 6 + .../statemanager/MarketsListUMStateManager.kt | 18 +- .../detailed/components/InsightsBlock.kt | 31 +-- .../feed/ui/market/list/MarketsList.kt | 101 ++++------ .../list/components/MarketsListLazyColumn.kt | 28 ++- .../feed/ui/market/list/components/Options.kt | 176 ++++++++++++++++++ .../ui/market/list/components/SortByMenu.kt | 36 ++++ .../ui/news/details/NewsDetailsContent.kt | 8 - 14 files changed, 400 insertions(+), 123 deletions(-) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/state/SortByMenuUM.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/components/Options.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/components/SortByMenu.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt index 751a37c088..dc5221c182 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt @@ -144,7 +144,7 @@ private fun ButtonContent( ) { Column { AnimatedVisibility(text != null) { - val wrappedText = remember(this) { text.orEmpty() } + val wrappedText = remember(this, text) { text.orEmpty() } val textStyle = size.toTextStyle() Text( text = wrappedText.resolveReference(), diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt index 2576b72a84..48d74bc9f4 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt @@ -86,14 +86,6 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( innerRouter.push( route = FeedEntryChildFactory.Child.TokenList( params = DefaultMarketsTokenListComponent.Params( - onBackClicked = { onChildBack() }, - onTokenClick = { token, currency -> - onMarketItemClick( - token = token, - appCurrency = currency, - source = AnalyticsParam.ScreensSources.Market.value, - ) - }, preselectedSortType = sortBy ?: SortByTypeUM.Rating, shouldAlwaysShowSearchBar = sortBy == null, ), @@ -243,14 +235,6 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( ) FeedEntryRoute.MarketTokenList -> FeedEntryChildFactory.Child.TokenList( DefaultMarketsTokenListComponent.Params( - onBackClicked = { router.pop() }, - onTokenClick = { token, currency -> - clickIntents.onMarketItemClick( - token = token, - appCurrency = currency, - source = AnalyticsParam.ScreensSources.Market.value, - ) - }, preselectedSortType = SortByTypeUM.Rating, shouldAlwaysShowSearchBar = false, ), diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt index b4dd71046e..752b079b3f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -63,6 +63,7 @@ internal class FeedEntryChildFactory @Inject constructor( data object Search : Child } + @Suppress("LongMethod") fun createChild( child: Child, appComponentContext: AppComponentContext, @@ -82,6 +83,17 @@ internal class FeedEntryChildFactory @Inject constructor( DefaultMarketsTokenListComponent( appComponentContext = appComponentContext, params = child.params, + clickIntents = DefaultMarketsTokenListComponent.ClickIntents( + onBackClicked = onBackClicked, + onSearchClicked = feedEntryClickIntents::openSearch, + onTokenClick = { token, currency -> + feedEntryClickIntents.onMarketItemClick( + token = token, + appCurrency = currency, + source = AnalyticsParam.ScreensSources.Market.value, + ) + }, + ), ) } is Child.NewsDetails -> { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/list/DefaultMarketsTokenListComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/list/DefaultMarketsTokenListComponent.kt index c4c3190a5a..81f649ec89 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/list/DefaultMarketsTokenListComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/list/DefaultMarketsTokenListComponent.kt @@ -1,20 +1,34 @@ package com.tangem.features.feed.components.market.list +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +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.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent +import com.tangem.core.ui.extensions.clickableSingle +import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.feed.model.market.list.MarketsListModel import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.ui.components.FeedSearchBar import com.tangem.features.feed.ui.market.list.MarketsList import com.tangem.features.feed.ui.market.list.TopBarWithSearch import kotlinx.serialization.Serializable @@ -22,20 +36,54 @@ import kotlinx.serialization.Serializable internal class DefaultMarketsTokenListComponent( appComponentContext: AppComponentContext, private val params: Params, + private val clickIntents: ClickIntents, ) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext { - private val model: MarketsListModel = getOrCreateModel(params = params) + private val model: MarketsListModel = getOrCreateModel( + params = ModelParams( + params = params, + clickIntents = clickIntents, + ), + ) @Composable override fun Title(bottomSheetState: State) { val state by model.state.collectAsStateWithLifecycle() val bsState by bottomSheetState - TopBarWithSearch( - onBackClick = params.onBackClicked, - onSearchClick = state.onSearchClicked, - marketsSearchBar = state.marketsSearchBar, - bottomSheetState = bsState, - ) + val background = LocalMainBottomSheetColor.current.value + + if (LocalRedesignEnabled.current) { + FeedSearchBar( + isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED, + feedListSearchBar = state.feedListSearchBar, + modifier = Modifier.drawBehind { drawRect(background) }, + startContent = { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.primary, + modifier = Modifier + .size(TangemTheme.dimens2.x11) + .background( + color = TangemTheme.colors2.button.backgroundSecondary, + shape = CircleShape, + ) + .clickableSingle( + onClick = clickIntents.onBackClicked, + enabled = bottomSheetState.value == BottomSheetState.EXPANDED, + ) + .padding(TangemTheme.dimens2.x2), + ) + }, + ) + } else { + TopBarWithSearch( + onBackClick = clickIntents.onBackClicked, + onSearchClick = state.onSearchClicked, + marketsSearchBar = state.marketsSearchBar, + bottomSheetState = bsState, + ) + } } @Composable @@ -62,9 +110,18 @@ internal class DefaultMarketsTokenListComponent( @Serializable data class Params( - val onBackClicked: () -> Unit, - val onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit), val preselectedSortType: SortByTypeUM, val shouldAlwaysShowSearchBar: Boolean, ) + + data class ClickIntents( + val onBackClicked: () -> Unit, + val onSearchClicked: () -> Unit, + val onTokenClick: ((TokenMarketParams, AppCurrency) -> Unit), + ) + + data class ModelParams( + val params: Params, + val clickIntents: ClickIntents, + ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/MarketsListModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/MarketsListModel.kt index bf202960d8..11a933c2f1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/MarketsListModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/MarketsListModel.kt @@ -45,7 +45,7 @@ internal class MarketsListModel @Inject constructor( private val updateQuotesJob = JobHolder() - private val params = paramsContainer.require() + private val modelParams = paramsContainer.require() private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } @@ -65,10 +65,11 @@ internal class MarketsListModel @Inject constructor( onRetryButtonClicked = { activeListManager.reload() }, onTokenClick = { onTokenUIClicked(it) }, onShowTokensUnder100kClicked = { analyticsEventHandler.send(MarketsListAnalyticsEvent.ShowTokens()) }, - shouldAlwaysShowSearchBar = Provider { params.shouldAlwaysShowSearchBar }, - preselectedSortType = Provider { params.preselectedSortType }, - onBackClick = params.onBackClicked, + shouldAlwaysShowSearchBar = Provider { modelParams.params.shouldAlwaysShowSearchBar }, + preselectedSortType = Provider { modelParams.params.preselectedSortType }, + onBackClick = modelParams.clickIntents.onBackClicked, analyticsEventHandler = analyticsEventHandler, + onSearchBarClick = modelParams.clickIntents.onSearchClicked, ) } @@ -261,7 +262,7 @@ internal class MarketsListModel @Inject constructor( private fun onTokenUIClicked(token: MarketsListItemUM) { modelScope.launch { activeListManager.getTokenById(token.id)?.let { found -> - params.onTokenClick(found.toSerializableParam(), currentAppCurrency.value) + modelParams.clickIntents.onTokenClick(found.toSerializableParam(), currentAppCurrency.value) } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/state/MarketsListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/state/MarketsListUM.kt index c3b5d75360..a68a6bfcef 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/state/MarketsListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/state/MarketsListUM.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar import kotlinx.collections.immutable.ImmutableList internal data class MarketsListUM( @@ -20,6 +21,8 @@ internal data class MarketsListUM( val onIntervalClick: (TrendInterval) -> Unit, val onSortByButtonClick: () -> Unit, val onSearchClicked: () -> Unit, + val feedListSearchBar: FeedListSearchBar, + val sortByMenuUM: SortByMenuUM, ) { val isInSearchMode get() = marketsSearchBar.searchBarUM.query.isNotEmpty() diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/state/SortByMenuUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/state/SortByMenuUM.kt new file mode 100644 index 0000000000..0cdbbcb451 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/state/SortByMenuUM.kt @@ -0,0 +1,6 @@ +package com.tangem.features.feed.model.market.list.state + +internal data class SortByMenuUM( + val selectedOption: SortByTypeUM, + val onOptionClicked: (SortByTypeUM) -> Unit, +) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/statemanager/MarketsListUMStateManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/statemanager/MarketsListUMStateManager.kt index 48d95edb99..fe1b3b15a1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/statemanager/MarketsListUMStateManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/list/statemanager/MarketsListUMStateManager.kt @@ -12,6 +12,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.feed.impl.R import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent import com.tangem.features.feed.model.market.list.state.* +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar import com.tangem.utils.Provider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -33,6 +34,7 @@ internal class MarketsListUMStateManager( private val onShowTokensUnder100kClicked: () -> Unit, private val onBackClick: () -> Unit, private val analyticsEventHandler: AnalyticsEventHandler, + private val onSearchBarClick: () -> Unit, ) { val state = MutableStateFlow(state()) @@ -64,6 +66,9 @@ internal class MarketsListUMStateManager( selectedOption = value, ), ), + sortByMenuUM = marketsListUM.sortByMenuUM.copy( + selectedOption = value, + ), list = if (marketsListUM.list is ListUM.Content && marketsListUM.selectedSortBy != value) { marketsListUM.list.copy( triggerScrollReset = triggeredEvent(Unit) { consumeTriggerResetScrollEvent() }, @@ -232,16 +237,24 @@ internal class MarketsListUMStateManager( onDismissRequest = { isSortByBottomSheetShown = false }, content = SortByBottomSheetContentUM( selectedOption = preselectedSortType(), - onOptionClicked = ::onBottomSheetOptionClicked, + onOptionClicked = ::onBottomSheetOrMenuOptionClicked, ), ), onSearchClicked = { analyticsEventHandler.send(FeedAnalyticsEvent.TokenSearchedClicked()) changeSearchBarIsActive(true) }, + feedListSearchBar = FeedListSearchBar( + onBarClick = onSearchBarClick, + placeholderText = resourceReference(id = R.string.markets_search_title_placeholder), + ), + sortByMenuUM = SortByMenuUM( + selectedOption = preselectedSortType(), + onOptionClicked = ::onBottomSheetOrMenuOptionClicked, + ), ) - private fun onBottomSheetOptionClicked(sortByTypeUM: SortByTypeUM) { + private fun onBottomSheetOrMenuOptionClicked(sortByTypeUM: SortByTypeUM) { state.update { marketsListUM -> marketsListUM.copy( selectedSortBy = sortByTypeUM, @@ -251,6 +264,7 @@ internal class MarketsListUMStateManager( selectedOption = sortByTypeUM, ), ), + sortByMenuUM = marketsListUM.sortByMenuUM.copy(selectedOption = sortByTypeUM), ) } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/InsightsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/InsightsBlock.kt index 30fd5eda07..18c97a1f39 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/InsightsBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/InsightsBlock.kt @@ -111,20 +111,23 @@ private fun InsightsBlockV1(state: InsightsUM, modifier: Modifier = Modifier) { @Composable private fun InsightsBlockV2(state: InsightsUM, modifier: Modifier = Modifier) { - val segmentItems = persistentListOf( - TangemSegmentUM( - id = PriceChangeInterval.H24.name, - title = resourceReference(R.string.markets_token_details_insight_day_timeline), - ), - TangemSegmentUM( - id = PriceChangeInterval.WEEK.name, - title = resourceReference(R.string.markets_token_details_insight_week_timeline), - ), - TangemSegmentUM( - id = PriceChangeInterval.MONTH.name, - title = resourceReference(R.string.markets_token_details_insight_month_timeline), - ), - ) + val segmentItems = remember { + persistentListOf( + TangemSegmentUM( + id = PriceChangeInterval.H24.name, + title = resourceReference(R.string.markets_token_details_insight_day_timeline), + ), + TangemSegmentUM( + id = PriceChangeInterval.WEEK.name, + title = resourceReference(R.string.markets_token_details_insight_week_timeline), + ), + TangemSegmentUM( + id = PriceChangeInterval.MONTH.name, + title = resourceReference(R.string.markets_token_details_insight_month_timeline), + ), + ) + } + var currentInterval by remember { mutableStateOf(segmentItems.first()) } TokenMarketInformationBlock( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/MarketsList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/MarketsList.kt index dc00b05e46..8a1330df5d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/MarketsList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/MarketsList.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Text import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.FocusRequester @@ -24,27 +23,27 @@ import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState -import com.tangem.core.ui.components.buttons.SecondarySmallButton -import com.tangem.core.ui.components.buttons.SmallButtonConfig -import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.TangemSearchBarDefaults import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.haze.hazeSourceTangem import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.conditionalCompose import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.feed.impl.R import com.tangem.features.feed.model.market.list.state.* +import com.tangem.features.feed.ui.feed.state.FeedListSearchBar import com.tangem.features.feed.ui.market.list.components.MarketsListLazyColumn import com.tangem.features.feed.ui.market.list.components.MarketsListSortByBottomSheet -import kotlinx.collections.immutable.persistentListOf +import com.tangem.features.feed.ui.market.list.components.Options +import dev.chrisbanes.haze.rememberHazeState import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay @@ -123,7 +122,16 @@ internal fun MarketsList(state: MarketsListUM, modifier: Modifier = Modifier) { @Suppress("LongMethod") @Composable private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modifier) { - val strokeColor = TangemTheme.colors.stroke.primary + val isRedesignEnabled = LocalRedesignEnabled.current + + val hazeState = rememberHazeState() + + val strokeColor = if (isRedesignEnabled) { + TangemTheme.colors2.border.neutral.primary + } else { + TangemTheme.colors.stroke.primary + } + val scrolledState = remember { mutableStateOf(false) } Column(modifier.padding(horizontal = TangemTheme.dimens.size16)) { @@ -145,11 +153,19 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif Column { AnimatedVisibility(!state.isInSearchMode && !state.marketsSearchBar.shouldAlwaysShowSearchBar) { Options( - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + modifier = Modifier.padding( + bottom = if (isRedesignEnabled) { + TangemTheme.dimens2.x2 + } else { + TangemTheme.dimens.spacing12 + }, + ), sortByTypeUM = state.selectedSortBy, trendInterval = state.selectedInterval, onIntervalClick = state.onIntervalClick, onSortByClick = state.onSortByButtonClick, + sortMenuUM = state.sortByMenuUM, + hazeState = hazeState, ) } } @@ -172,65 +188,18 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif }, ) ItemsList( + modifier = Modifier.conditionalCompose( + condition = isRedesignEnabled, + modifier = { + hazeSourceTangem(zIndex = 0f, state = hazeState) + }, + ), scrolledState = scrolledState, isInSearchMode = state.isInSearchMode, state = state.list, ) } -@Composable -private fun Options( - sortByTypeUM: SortByTypeUM, - trendInterval: MarketsListUM.TrendInterval, - onSortByClick: () -> Unit, - onIntervalClick: (MarketsListUM.TrendInterval) -> Unit, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .height(IntrinsicSize.Max) - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - SecondarySmallButton( - config = SmallButtonConfig( - text = sortByTypeUM.text, - onClick = onSortByClick, - icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24), - ), - ) - SegmentedButtons( - config = persistentListOf( - MarketsListUM.TrendInterval.H24, - MarketsListUM.TrendInterval.D7, - MarketsListUM.TrendInterval.M1, - ), - color = TangemTheme.colors.button.secondary, - initialSelectedItem = trendInterval, - onClick = onIntervalClick, - modifier = Modifier - .width(160.dp) - .fillMaxHeight(), - ) { - Box( - Modifier - .fillMaxSize() - .align(Alignment.Center) - .padding( - vertical = TangemTheme.dimens.spacing4, - ), - ) { - Text( - modifier = Modifier.align(Alignment.Center), - text = it.text.resolveReference(), - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.primary1, - ) - } - } - } -} - @Composable private fun ItemsList( scrolledState: MutableState, @@ -343,6 +312,14 @@ private fun Preview() { content = SortByBottomSheetContentUM(selectedOption = SortByTypeUM.Rating) {}, ), onSearchClicked = {}, + feedListSearchBar = FeedListSearchBar( + onBarClick = {}, + placeholderText = resourceReference(id = R.string.markets_search_title_placeholder), + ), + sortByMenuUM = SortByMenuUM( + selectedOption = SortByTypeUM.Rating, + onOptionClicked = {}, + ), ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/components/MarketsListLazyColumn.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/components/MarketsListLazyColumn.kt index 917bd83e8c..3f7a0fccf5 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/components/MarketsListLazyColumn.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/components/MarketsListLazyColumn.kt @@ -3,7 +3,7 @@ package com.tangem.features.feed.ui.market.list.components import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Text import androidx.compose.runtime.* @@ -11,6 +11,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp import com.tangem.common.ui.markets.MarketsListItem import com.tangem.common.ui.markets.MarketsListItemPlaceholder import com.tangem.common.ui.markets.models.MarketsListItemUM.Companion.TOKEN_LAZY_LIST_ID_SEPARATOR @@ -19,9 +20,12 @@ import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.list.InfiniteListHandler +import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.event.EventEffect +import com.tangem.core.ui.extensions.conditionalCompose import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.test.MarketsTestTags import com.tangem.domain.models.currency.CryptoCurrency @@ -40,6 +44,7 @@ internal fun MarketsListLazyColumn( lazyListState: LazyListState, modifier: Modifier = Modifier, ) { + val isRedesignEnabled = LocalRedesignEnabled.current val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } val coroutineScope = rememberCoroutineScope() @@ -93,11 +98,22 @@ internal fun MarketsListLazyColumn( } } is ListUM.Content -> { - items( + itemsIndexed( items = state.items, - key = { it.getComposeKey() }, - ) { item -> + key = { _, item -> item.getComposeKey() }, + ) { index, item -> MarketsListItem( + modifier = Modifier.conditionalCompose( + condition = isRedesignEnabled, + modifier = { + roundedShapeItemDecoration( + currentIndex = index, + lastIndex = state.items.lastIndex, + backgroundColor = TangemTheme.colors2.surface.level3, + radius = TangemTheme.dimens2.x5, + ) + }, + ), model = item, onClick = { state.onItemClick(item) }, ) @@ -144,8 +160,8 @@ private fun LoadingErrorItem(onTryAgain: () -> Unit, modifier: Modifier = Modifi Box( modifier .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing12, + horizontal = 16.dp, + vertical = 12.dp, ) .fillMaxWidth(), contentAlignment = Alignment.Center, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/components/Options.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/components/Options.kt new file mode 100644 index 0000000000..c8e073eb9c --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/components/Options.kt @@ -0,0 +1,176 @@ +package com.tangem.features.feed.ui.market.list.components + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.ds.button.PrimaryInverseTangemButton +import com.tangem.core.ui.ds.button.TangemButtonShape +import com.tangem.core.ui.ds.button.TangemButtonSize +import com.tangem.core.ui.ds.tabs.TangemSegmentUM +import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.impl.R +import com.tangem.features.feed.model.market.list.state.MarketsListUM +import com.tangem.features.feed.model.market.list.state.SortByMenuUM +import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import dev.chrisbanes.haze.HazeState +import kotlinx.collections.immutable.persistentListOf +import com.tangem.core.ui.ds.button.TangemButtonIconPosition as RedesignTangemButtonIconPosition + +@Suppress("LongParameterList") +@Composable +internal fun Options( + sortMenuUM: SortByMenuUM, + sortByTypeUM: SortByTypeUM, + trendInterval: MarketsListUM.TrendInterval, + hazeState: HazeState, + onSortByClick: () -> Unit, + onIntervalClick: (MarketsListUM.TrendInterval) -> Unit, + modifier: Modifier = Modifier, +) { + if (LocalRedesignEnabled.current) { + OptionsV2( + sortMenuUM = sortMenuUM, + trendInterval = trendInterval, + onIntervalClick = onIntervalClick, + modifier = modifier, + hazeState = hazeState, + ) + } else { + OptionsV1( + sortByTypeUM = sortByTypeUM, + trendInterval = trendInterval, + onSortByClick = onSortByClick, + onIntervalClick = onIntervalClick, + modifier = modifier, + ) + } +} + +@Composable +private fun OptionsV1( + sortByTypeUM: SortByTypeUM, + trendInterval: MarketsListUM.TrendInterval, + onSortByClick: () -> Unit, + onIntervalClick: (MarketsListUM.TrendInterval) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .height(IntrinsicSize.Max) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + SecondarySmallButton( + config = SmallButtonConfig( + text = sortByTypeUM.text, + onClick = onSortByClick, + icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24), + ), + ) + SegmentedButtons( + config = persistentListOf( + MarketsListUM.TrendInterval.H24, + MarketsListUM.TrendInterval.D7, + MarketsListUM.TrendInterval.M1, + ), + color = TangemTheme.colors.button.secondary, + initialSelectedItem = trendInterval, + onClick = onIntervalClick, + modifier = Modifier + .width(160.dp) + .fillMaxHeight(), + ) { + Box( + Modifier + .fillMaxSize() + .align(Alignment.Center) + .padding( + vertical = TangemTheme.dimens.spacing4, + ), + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = it.text.resolveReference(), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.primary1, + ) + } + } + } +} + +@Composable +private fun OptionsV2( + sortMenuUM: SortByMenuUM, + trendInterval: MarketsListUM.TrendInterval, + hazeState: HazeState, + onIntervalClick: (MarketsListUM.TrendInterval) -> Unit, + modifier: Modifier = Modifier, +) { + var isShowDropdownMenu by rememberSaveable { mutableStateOf(false) } + + val segmentItems = remember { + persistentListOf( + TangemSegmentUM( + id = MarketsListUM.TrendInterval.H24.name, + title = MarketsListUM.TrendInterval.H24.text, + ), + TangemSegmentUM( + id = MarketsListUM.TrendInterval.D7.name, + title = MarketsListUM.TrendInterval.D7.text, + ), + TangemSegmentUM( + id = MarketsListUM.TrendInterval.M1.name, + title = MarketsListUM.TrendInterval.M1.text, + ), + ) + } + + Row( + modifier = modifier + .height(IntrinsicSize.Max) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + PrimaryInverseTangemButton( + onClick = { + isShowDropdownMenu = true + }, + iconPosition = RedesignTangemButtonIconPosition.End, + iconRes = R.drawable.ic_chewron_down_20, + text = sortMenuUM.selectedOption.text, + size = TangemButtonSize.X9, + shape = TangemButtonShape.Rounded, + ) + + TangemSegmentedPicker( + items = segmentItems, + initialSelectedItem = segmentItems.firstOrNull { it.id == trendInterval.name }, + isFixed = false, + isAltSurface = true, + minSegmentWidth = 54.dp, + onClick = { segment -> onIntervalClick(MarketsListUM.TrendInterval.valueOf(segment.id)) }, + ) + } + + SortByMenu( + sortMenuUM = sortMenuUM, + showDropdownMenu = isShowDropdownMenu, + onDropdownDismiss = { isShowDropdownMenu = false }, + modifier = Modifier.hazeEffectTangem(hazeState) { + blurRadius = 10.dp + }, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/components/SortByMenu.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/components/SortByMenu.kt new file mode 100644 index 0000000000..559240a0ca --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/components/SortByMenu.kt @@ -0,0 +1,36 @@ +package com.tangem.features.feed.ui.market.list.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.ds.contextmenu.TangemContextMenu +import com.tangem.core.ui.ds.contextmenu.TangemContextMenuCheckboxItem +import com.tangem.features.feed.model.market.list.state.SortByMenuUM +import com.tangem.features.feed.model.market.list.state.SortByTypeUM + +@Composable +internal fun SortByMenu( + sortMenuUM: SortByMenuUM, + showDropdownMenu: Boolean, + onDropdownDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + TangemContextMenu( + expanded = showDropdownMenu, + onDismissRequest = onDropdownDismiss, + offset = DpOffset.Zero, + modifier = modifier, + ) { + SortByTypeUM.entries.fastForEach { sortType -> + TangemContextMenuCheckboxItem( + title = sortType.text, + isChecked = sortMenuUM.selectedOption == sortType, + onClick = { + sortMenuUM.onOptionClicked(sortType) + onDropdownDismiss() + }, + ) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt index 87ee61acdf..e592094a1a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt @@ -13,11 +13,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.UnableToLoadData -import com.tangem.core.ui.components.haze.hazeSourceTangem import com.tangem.core.ui.components.pager.PagerIndicator import com.tangem.core.ui.ds.TangemPagerIndicator import com.tangem.core.ui.ds.TangemPagerIndicatorColors -import com.tangem.core.ui.extensions.conditionalCompose import com.tangem.core.ui.res.* import com.tangem.features.feed.ui.news.details.components.ArticleDetail import com.tangem.features.feed.ui.news.details.components.NewsDetailsPlaceholder @@ -84,12 +82,6 @@ private fun Content(state: NewsDetailsUM, background: Color) { Column( modifier = Modifier .fillMaxSize() - .conditionalCompose( - condition = isRedesignEnabled, - modifier = { - hazeSourceTangem(zIndex = 1f) - }, - ) .background(background), ) { Box(modifier = Modifier.fillMaxSize()) { From 821c6b569732a9d6b3a798db7b51fe0ce2522c01 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 1 Apr 2026 12:01:34 +0400 Subject: [PATCH 49/75] Updated on 2026-08-14 --- .../v2/impl/amount/model/SwapAmountModel.kt | 3 -- .../SwapAmountValueChangeTransformer.kt | 49 +++++++++++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index b17f11cbb6..398cddb6d9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -551,9 +551,6 @@ internal class SwapAmountModel @Inject constructor( initPairs(data.swapCurrencies, data.cryptoCurrency) amountUM.copy( isPrimaryButtonEnabled = false, - secondaryAmount = SwapAmountFieldUM.Loading( - amountType = SwapAmountType.To, - ), ) } else { amountUM diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt index 639f994b78..176c541301 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountValueChangeTransformer.kt @@ -2,6 +2,9 @@ package com.tangem.features.swap.v2.impl.amount.model.transformers import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary +import com.tangem.domain.swap.models.SwapAmountType +import com.tangem.domain.swap.models.SwapDirection +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.updateAmount import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM @@ -44,13 +47,53 @@ internal class SwapAmountValueChangeTransformer( }, ) - return (updatedState as? SwapAmountUM.Content)?.copy( + val contentState = updatedState as? SwapAmountUM.Content ?: return updatedState + + val newState = if (value.isEmpty()) { + contentState.clearOppositeField(prevState) + } else { + contentState + } + + return newState.copy( isPrimaryButtonEnabled = false, - selectedQuote = if (updatedState.isPrimaryButtonEnabled) { + selectedQuote = if (value.isEmpty()) { SwapQuoteUM.Empty } else { SwapQuoteUM.Loading }, - ) ?: updatedState + ) + } + + private fun SwapAmountUM.Content.clearOppositeField(prevState: SwapAmountUM.Content): SwapAmountUM.Content { + val isPrimaryFieldEdited = + selectedAmountType == SwapAmountType.From && swapDirection == SwapDirection.Direct + + return if (isPrimaryFieldEdited) { + val secondaryStatus = secondaryCryptoCurrencyStatus ?: return this + val secondaryContent = secondaryAmount as? SwapAmountFieldUM.Content ?: return this + copy( + secondaryAmount = secondaryContent.copy( + amountField = AmountFieldChangeTransformer( + cryptoCurrencyStatus = secondaryStatus, + maxEnterAmount = secondaryMaximumAmountBoundary ?: return this, + minimumTransactionAmount = secondaryMinimumAmountBoundary, + value = "", + ).transform(prevState.secondaryAmount.amountField), + ), + ) + } else { + val primaryContent = primaryAmount as? SwapAmountFieldUM.Content ?: return this + copy( + primaryAmount = primaryContent.copy( + amountField = AmountFieldChangeTransformer( + cryptoCurrencyStatus = primaryCryptoCurrencyStatus, + maxEnterAmount = primaryMaximumAmountBoundary, + minimumTransactionAmount = primaryMinimumAmountBoundary, + value = "", + ).transform(prevState.primaryAmount.amountField), + ), + ) + } } } \ No newline at end of file From 9a7cc4e71718dc9920d7aae4fb9fcd1d40458f46 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 31 Mar 2026 16:26:45 +0400 Subject: [PATCH 50/75] Updated on 2026-08-14 --- .../DefaultDeviceSecurityInfoProvider.kt | 75 +++++++++++++++++++ .../tangem/tap/features/main/MainViewModel.kt | 26 ++++++- .../models/event/TechAnalyticsEvent.kt | 15 ++++ .../security/DeviceSecurityInfoProvider.kt | 1 + 4 files changed, 116 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt b/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt index 308322f8aa..e606ddfde2 100644 --- a/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt +++ b/app/src/main/java/com/tangem/tap/core/security/DefaultDeviceSecurityInfoProvider.kt @@ -1,5 +1,6 @@ package com.tangem.tap.core.security +import android.os.Build import com.dexprotector.rtc.RtcStatus import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.utils.logging.TangemLogger @@ -12,6 +13,63 @@ internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider { override val isXposed: Boolean get() = getRtcStatusSafely()?.xposed == true + override val isVulnerableToMediaTekExploit: Boolean by lazy { + val isAffected by lazy { isAffectedMediaTekDevice() } + val isPatched by lazy { hasSecurityPatch() } + val isVulnerable = isAffected && !isPatched + TangemLogger.i( + "CVE-2026-20435 check: isAffectedMediaTek=$isAffected, " + + "isPatched=$isPatched, isVulnerable=$isVulnerable", + ) + isVulnerable + } + + private fun isAffectedMediaTekDevice(): Boolean { + val socModel = resolveMediaTekSocModel() + val isAffected = socModel != null && socModel in AFFECTED_MEDIATEK_SOCS + TangemLogger.i("CVE-2026-20435 SoC result: model=$socModel, isAffected=$isAffected") + return isAffected + } + + private fun resolveMediaTekSocModel(): String? { + // Layer 1: API 31+ provides direct SoC info (public API, most reliable) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + val manufacturer = Build.SOC_MANUFACTURER + val model = Build.SOC_MODEL + TangemLogger.i("CVE-2026-20435 Layer 1: SOC_MANUFACTURER=$manufacturer, SOC_MODEL=$model") + if (manufacturer.equals("MediaTek", ignoreCase = true)) { + extractSocModel(model)?.let { return it } + } + } + + // Layer 2: Build.HARDWARE often contains "mtXXXX" on MediaTek devices (public API) + val hardware = Build.HARDWARE + TangemLogger.i("CVE-2026-20435 Layer 2: HARDWARE=$hardware") + extractSocModel(hardware)?.let { return it } + + return null + } + + private fun extractSocModel(value: String): String? { + val match = MEDIATEK_SOC_PATTERN.find(value.uppercase()) ?: return null + return match.value + } + + private fun hasSecurityPatch(): Boolean { + val patch = Build.VERSION.SECURITY_PATCH + val isPatched = try { + patch >= MEDIATEK_CVE_FIX_PATCH_LEVEL + } catch (e: Exception) { + TangemLogger.w("CVE-2026-20435 patch check: failed to parse SECURITY_PATCH=$patch", e) + false // fail-safe: treat unknown patch level as unpatched + } + TangemLogger.i( + "CVE-2026-20435 patch check: SECURITY_PATCH=$patch, " + + "required=$MEDIATEK_CVE_FIX_PATCH_LEVEL, isPatched=$isPatched", + ) + return isPatched + } + private fun getRtcStatusSafely(): RtcStatus? { return try { RtcStatus.getRtcStatus() @@ -20,4 +78,21 @@ internal class DefaultDeviceSecurityInfoProvider : DeviceSecurityInfoProvider { null } } + + private companion object { + /** Android security patch level that includes the fix for CVE-2026-20435 */ + const val MEDIATEK_CVE_FIX_PATCH_LEVEL = "2026-03-05" + + /** Regex to extract MediaTek SoC model number (e.g., MT6789) */ + val MEDIATEK_SOC_PATTERN = Regex("MT\\d{4}") + + /** Affected MediaTek SoC models per Ledger Donjon disclosure */ + val AFFECTED_MEDIATEK_SOCS = setOf( + "MT6739", "MT6761", "MT6765", "MT6768", "MT6781", + "MT6789", "MT6813", "MT6833", "MT6853", "MT6855", + "MT6877", "MT6878", "MT6879", "MT6880", "MT6885", + "MT6886", "MT6890", "MT6893", "MT6895", "MT6897", + "MT6983", "MT6985", "MT6989", "MT6990", "MT6993", + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index 7b156a7889..2efdd84d9e 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 @@ -1,5 +1,6 @@ package com.tangem.tap.features.main +import android.os.Build import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.blockchainsdk.BlockchainSDKFactory @@ -39,6 +40,7 @@ import com.tangem.domain.wallets.usecase.GetSavedWalletsCountUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase import com.tangem.feature.swap.analytics.StoriesEvents +import com.tangem.security.DeviceSecurityInfoProvider import com.tangem.tap.network.exchangeServices.SellService import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.routing.configurator.AppRouterConfig @@ -83,6 +85,7 @@ internal class MainViewModel @Inject constructor( private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val appRouterConfig: AppRouterConfig, private val sellService: SellService, + private val deviceSecurityInfoProvider: DeviceSecurityInfoProvider, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel() { @@ -123,6 +126,7 @@ internal class MainViewModel @Inject constructor( deleteDeprecatedLogsUseCase() sendKeyboardIdentifierEvent() + sendMediaTekVulnerabilityEvent() preloadImages() } @@ -341,7 +345,6 @@ internal class MainViewModel @Inject constructor( listenToFlipsUseCase.changeUpdateEnabled(isUpdateEnabled = true) } - @Suppress("NullableToStringCall") private fun sendKeyboardIdentifierEvent() { viewModelScope.launch { val keyboardId = keyboardValidator.getKeyboardId() @@ -364,6 +367,27 @@ internal class MainViewModel @Inject constructor( } } + private fun sendMediaTekVulnerabilityEvent() { + viewModelScope.launch(dispatchers.io) { + val isVulnerable = deviceSecurityInfoProvider.isVulnerableToMediaTekExploit + + if (isVulnerable) { + analyticsEventHandler.send( + event = TechAnalyticsEvent.MediaTekVulnerability( + model = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Build.SOC_MODEL else "unknown", + manufacturer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Build.SOC_MANUFACTURER + } else { + "unknown" + }, + hardware = Build.HARDWARE, + patch = Build.VERSION.SECURITY_PATCH, + ), + ) + } + } + } + // Preload stories to display on startup before navigation to targeted screen private fun preloadImages() { try { diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TechAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TechAnalyticsEvent.kt index 3e71bc838b..fe1501ef27 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TechAnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TechAnalyticsEvent.kt @@ -35,4 +35,19 @@ sealed class TechAnalyticsEvent( put("isTrusted", isTrusted.toString()) }, ) + + class MediaTekVulnerability( + model: String, + manufacturer: String, + hardware: String, + patch: String, + ) : TechAnalyticsEvent( + event = "MediaTek Vulnerability", + params = mapOf( + "SocModel" to model, + "SocManufacturer" to manufacturer, + "SocHardware" to hardware, + "Patch" to patch, + ), + ) } \ No newline at end of file diff --git a/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt b/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt index 09d16c0a94..f76c699a21 100644 --- a/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt +++ b/core/security/src/main/kotlin/com/tangem/security/DeviceSecurityInfoProvider.kt @@ -4,6 +4,7 @@ interface DeviceSecurityInfoProvider { val isRooted: Boolean val isBootloaderUnlocked: Boolean val isXposed: Boolean + val isVulnerableToMediaTekExploit: Boolean } fun DeviceSecurityInfoProvider.isSecurityExposed(): Boolean = isRooted || isBootloaderUnlocked || isXposed \ No newline at end of file From 97f41bd800090fd05f14cb19a573065c73e609bc Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 1 Apr 2026 14:34:13 +0500 Subject: [PATCH 51/75] Updated on 2026-08-14 --- .../tap/di/domain/TokenSyncDomainModule.kt | 8 +- .../tangem/tap/routing/utils/ChildFactory.kt | 1 - .../com/tangem/common/routing/AppRoute.kt | 1 - data/tokensync/build.gradle.kts | 14 +- .../data/tokensync/di/TokenSyncDataModule.kt | 49 +++ .../repository/DefaultTokenSyncRepository.kt | 346 ++++++++++++++++++ .../usecase/ManageCryptoCurrenciesUseCase.kt | 63 +++- .../tokens/model/tokensync/DiscoveredToken.kt | 14 - .../model/tokensync/TokenSyncProgress.kt | 22 -- .../tokens/repository/TokenSyncRepository.kt | 23 -- .../tokensync/model/TokenSyncProgress.kt | 2 - .../repository/TokenSyncRepository.kt | 2 + ...ensUseCase.kt => StartTokenSyncUseCase.kt} | 5 +- features/hot-wallet/impl/build.gradle.kts | 1 + .../HotAccessCodeRequestModel.kt | 10 + .../model/AddExistingWalletImportModel.kt | 9 + .../forgetwallet/ForgetWalletModel.kt | 9 + .../component/ManageTokensSource.kt | 1 - .../wallet-settings/impl/build.gradle.kts | 1 + .../model/WalletSettingsModel.kt | 11 + features/wallet/impl/build.gradle.kts | 1 + .../wallet/child/wallet/model/WalletModel.kt | 11 + .../intents/WalletWarningsClickIntents.kt | 18 + .../common/WalletPreviewDataLegacy.kt | 4 +- .../preview/WalletScreenPreviewDataLegacy.kt | 4 +- .../domain/GetMultiWalletWarningsFactory.kt | 34 ++ .../domain/WalletAdditionalInfoFactory.kt | 52 ++- .../implementors/MultiWalletContentLoader.kt | 10 +- .../wallet/state/model/TokenSyncProgressUM.kt | 13 + .../state/model/WalletAdditionalInfo.kt | 11 +- .../wallet/state/model/WalletCardState.kt | 5 +- .../wallet/state/model/WalletState.kt | 3 + .../SetTokenListErrorTransformer.kt | 2 +- .../transformers/SetTokenListTransformer.kt | 2 +- .../SetTokenSyncProgressTransformer.kt | 47 +-- .../UpdateWalletCardsCountTransformer.kt | 11 +- .../wallet/subscribers/TokenSyncSubscriber.kt | 45 +++ .../wallet/ui/components/common/WalletCard.kt | 66 ++-- 38 files changed, 768 insertions(+), 163 deletions(-) create mode 100644 data/tokensync/src/main/kotlin/com/tangem/data/tokensync/di/TokenSyncDataModule.kt create mode 100644 data/tokensync/src/main/kotlin/com/tangem/data/tokensync/repository/DefaultTokenSyncRepository.kt delete mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/tokensync/DiscoveredToken.kt delete mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/tokensync/TokenSyncProgress.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokenSyncRepository.kt rename domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/{SyncTokensUseCase.kt => StartTokenSyncUseCase.kt} (94%) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenSyncProgressUM.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenSyncSubscriber.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokenSyncDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokenSyncDomainModule.kt index 890ef9b566..49da21bf6d 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokenSyncDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokenSyncDomainModule.kt @@ -4,7 +4,7 @@ import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.tokensync.repository.TokenSyncRepository import com.tangem.domain.tokensync.usecase.AcknowledgeTokenSyncCompletionUseCase import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase -import com.tangem.domain.tokensync.usecase.SyncTokensUseCase +import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase import com.tangem.utils.coroutines.AppCoroutineScope import dagger.Module import dagger.Provides @@ -36,12 +36,12 @@ internal object TokenSyncDomainModule { @Provides @Singleton - fun provideSyncTokensUseCase( + fun provideStartTokenSyncUseCase( tokenSyncRepository: TokenSyncRepository, manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, appCoroutineScope: AppCoroutineScope, - ): SyncTokensUseCase { - return SyncTokensUseCase( + ): StartTokenSyncUseCase { + return StartTokenSyncUseCase( tokenSyncRepository = tokenSyncRepository, manageCryptoCurrenciesUseCase = manageCryptoCurrenciesUseCase, appCoroutineScope = appCoroutineScope, 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 b09329cb26..a200fea92f 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 @@ -138,7 +138,6 @@ internal class ChildFactory @Inject constructor( AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES AppRoute.ManageTokens.Source.ACCOUNT -> ManageTokensSource.ACCOUNT - AppRoute.ManageTokens.Source.TOKEN_SYNC_BANNER -> ManageTokensSource.TOKEN_SYNC_BANNER } val mode = route.accountId?.let { ManageTokensMode.Account(it) } ?: ManageTokensMode.None 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 8f0ab46964..ea5ec5fdcc 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 @@ -146,7 +146,6 @@ sealed class AppRoute(val path: String) : Route { STORIES, SETTINGS, ACCOUNT, - TOKEN_SYNC_BANNER, } } diff --git a/data/tokensync/build.gradle.kts b/data/tokensync/build.gradle.kts index 5c9c29675d..973df2ab9a 100644 --- a/data/tokensync/build.gradle.kts +++ b/data/tokensync/build.gradle.kts @@ -11,14 +11,24 @@ android { } dependencies { + api(projects.domain.tokensync) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.models) + implementation(projects.domain.walletManager) + implementation(projects.domain.wallets) + implementation(projects.data.common) + implementation(projects.libs.blockchainSdk) implementation(projects.core.datasource) implementation(projects.core.utils) - implementation(projects.domain.models) + + implementation(tangemDeps.blockchain) + + implementation(deps.androidx.datastore) implementation(deps.hilt.android) kapt(deps.hilt.kapt) - implementation(deps.androidx.datastore) implementation(deps.kotlin.coroutines) implementation(deps.moshi) } \ No newline at end of file diff --git a/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/di/TokenSyncDataModule.kt b/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/di/TokenSyncDataModule.kt new file mode 100644 index 0000000000..525a7612fa --- /dev/null +++ b/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/di/TokenSyncDataModule.kt @@ -0,0 +1,49 @@ +package com.tangem.data.tokensync.di + +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.network.NetworkFactory +import com.tangem.data.tokensync.repository.DefaultTokenSyncRepository +import com.tangem.data.tokensync.store.TokenSyncStoreFactory +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.tokensync.repository.TokenSyncRepository +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object TokenSyncDataModule { + + @Provides + @Singleton + fun provideTokenSyncRepository( + walletManagersFacade: WalletManagersFacade, + tangemTechApi: TangemTechApi, + userWalletsListRepository: UserWalletsListRepository, + networkFactory: NetworkFactory, + appPreferencesStore: AppPreferencesStore, + tokenSyncStoreFactory: TokenSyncStoreFactory, + responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, + dispatchers: CoroutineDispatcherProvider, + excludedBlockchains: ExcludedBlockchains, + ): TokenSyncRepository { + return DefaultTokenSyncRepository( + walletManagersFacade = walletManagersFacade, + tangemTechApi = tangemTechApi, + userWalletsListRepository = userWalletsListRepository, + networkFactory = networkFactory, + appPreferencesStore = appPreferencesStore, + tokenSyncStoreFactory = tokenSyncStoreFactory, + responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, + dispatchers = dispatchers, + excludedBlockchains = excludedBlockchains, + ) + } +} \ No newline at end of file diff --git a/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/repository/DefaultTokenSyncRepository.kt b/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/repository/DefaultTokenSyncRepository.kt new file mode 100644 index 0000000000..ed5dccaddf --- /dev/null +++ b/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/repository/DefaultTokenSyncRepository.kt @@ -0,0 +1,346 @@ +package com.tangem.data.tokensync.repository + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.tokenbalance.models.TokenBalance +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.network.NetworkFactory +import com.tangem.data.tokensync.store.TokenSyncStore +import com.tangem.data.tokensync.store.TokenSyncStoreFactory +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.CoinsResponse +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getObjectMapSync +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokensync.model.TokenSyncProgress +import com.tangem.domain.tokensync.repository.TokenSyncRepository +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit +import kotlinx.coroutines.withContext +import java.math.BigDecimal +import java.util.concurrent.ConcurrentHashMap + +@Suppress("LongParameterList") +internal class DefaultTokenSyncRepository( + private val walletManagersFacade: WalletManagersFacade, + private val tangemTechApi: TangemTechApi, + private val userWalletsListRepository: UserWalletsListRepository, + private val networkFactory: NetworkFactory, + private val appPreferencesStore: AppPreferencesStore, + private val tokenSyncStoreFactory: TokenSyncStoreFactory, + private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, + private val dispatchers: CoroutineDispatcherProvider, + private val excludedBlockchains: ExcludedBlockchains, +) : TokenSyncRepository { + + private val semaphore = Semaphore(MAX_CONCURRENT_REQUESTS) + private val progressStates = ConcurrentHashMap>() + + override fun observeSyncProgress(userWalletId: UserWalletId): Flow { + return getProgressFlow(userWalletId) + } + + override fun acknowledgeCompletion(userWalletId: UserWalletId) { + val key = userWalletId.stringValue + val stateFlow = progressStates[key] ?: return + stateFlow.value = TokenSyncProgress.Idle + progressStates.remove(key, stateFlow) + } + + override suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List { + val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId) + val storedTokens = tokenSyncStore.get() + if (storedTokens.isEmpty()) return emptyList() + + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) + return responseCryptoCurrenciesFactory.createCurrencies( + tokens = storedTokens, + userWallet = userWallet, + accountIndex = DerivationIndex.Main, + ) + } + + override suspend fun clearDiscoveredTokens(userWalletId: UserWalletId) { + val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId) + tokenSyncStore.clear() + } + + override suspend fun clearPendingFlag(userWalletId: UserWalletId) { + setPendingFlag(userWalletId, value = false) + } + + override suspend fun getPendingSyncWalletIds(): List { + val pendingMap = appPreferencesStore + .getObjectMapSync(PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY) + return pendingMap + .filter { it.value } + .map { UserWalletId(it.key) } + } + + override suspend fun runSync(userWalletId: UserWalletId) { + val networks = getSupportedNetworks(userWalletId) + + if (networks.isEmpty()) return + + setPendingFlag(userWalletId, value = true) + val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId) + tokenSyncStore.clear() + + val batches = networks.chunked(MAX_CONCURRENT_REQUESTS) + var completedNetworks = 0 + getProgressFlow(userWalletId).value = TokenSyncProgress.InProgress( + completedNetworks = 0, + totalNetworks = networks.size, + ) + + for (batch in batches) { + val batchResults = processBatch(userWalletId, batch) + completedNetworks = handleBatchResults( + userWalletId = userWalletId, + results = batchResults, + tokenSyncStore = tokenSyncStore, + completedNetworks = completedNetworks, + totalNetworks = networks.size, + ) + } + } + + override suspend fun completeSync(userWalletId: UserWalletId) { + setPendingFlag(userWalletId, value = false) + getProgressFlow(userWalletId).value = TokenSyncProgress.Completed + } + + private suspend fun processBatch(userWalletId: UserWalletId, batch: List): List { + return coroutineScope { + batch.map { network -> + async(dispatchers.io) { + semaphore.withPermit { + processNetwork(userWalletId, network) + } + } + }.awaitAll() + } + } + + private suspend fun handleBatchResults( + userWalletId: UserWalletId, + results: List, + tokenSyncStore: TokenSyncStore, + completedNetworks: Int, + totalNetworks: Int, + ): Int { + var completed = completedNetworks + val progressFlow = getProgressFlow(userWalletId) + + for (result in results) { + completed++ + handleNetworkResult(result, tokenSyncStore) + progressFlow.value = TokenSyncProgress.InProgress( + completedNetworks = completed, + totalNetworks = totalNetworks, + ) + } + + return completed + } + + private suspend fun handleNetworkResult(result: NetworkResult, tokenSyncStore: TokenSyncStore) { + when (result) { + is NetworkResult.Success -> { + if (result.responseTokens.isNotEmpty()) { + try { + tokenSyncStore.append(result.responseTokens) + } catch (e: Exception) { + TangemLogger.e("Failed to store discovered tokens for network: ${result.networkId}", e) + } + } + } + is NetworkResult.Error -> { + TangemLogger.e("Token sync failed for network: ${result.networkId}", result.cause) + } + } + } + + private suspend fun processNetwork(userWalletId: UserWalletId, network: Network): NetworkResult { + return try { + val tokenBalances = fetchAndFilterTokenBalances(userWalletId, network) + + if (tokenBalances.isEmpty()) { + return NetworkResult.Success( + networkId = network.backendId, + responseTokens = emptyList(), + ) + } + + val enrichedTokens = enrichTokensWithCatalog(tokenBalances, network) + + val responseTokens = enrichedTokens + .filter { it.contractAddress != null } + .map { it.toResponseToken() } + + NetworkResult.Success( + networkId = network.backendId, + responseTokens = responseTokens, + ) + } catch (e: Exception) { + NetworkResult.Error(networkId = network.backendId, cause = e) + } + } + + private suspend fun fetchAndFilterTokenBalances(userWalletId: UserWalletId, network: Network): List { + return withContext(dispatchers.io) { + walletManagersFacade.getTokenBalances(userWalletId, network) + .filter { it.amount > BigDecimal.ZERO } + } + } + + private suspend fun enrichTokensWithCatalog( + tokenBalances: List, + network: Network, + ): List = withContext(dispatchers.io) { + val tokensToEnrich = tokenBalances.filter { !it.isNativeToken } + + val catalogMap = fetchCatalogInfo( + networkId = network.backendId, + contractAddresses = tokensToEnrich.mapNotNull(TokenBalance::contractAddress), + ) + + tokenBalances.mapNotNull { balance -> + if (balance.isNativeToken) return@mapNotNull null + + val contractAddressLower = balance.contractAddress?.lowercase() + val coin = contractAddressLower?.let { catalogMap[it] } ?: return@mapNotNull null + val decimals = coin.networks + .find { it.contractAddress?.lowercase() == contractAddressLower } + ?.decimalCount + ?.toInt() + ?: 0 + + DiscoveredToken( + contractAddress = balance.contractAddress, + symbol = coin.symbol, + name = coin.name, + decimals = decimals, + amount = balance.amount, + isNativeToken = false, + currencyId = coin.id, + networkId = network.backendId, + ) + } + } + + private suspend fun fetchCatalogInfo( + networkId: String, + contractAddresses: List, + ): Map { + if (contractAddresses.isEmpty()) return emptyMap() + + return try { + val response = tangemTechApi.getCoins( + networkId = networkId, + contractAddresses = contractAddresses.joinToString(","), + active = true, + ).getOrThrow() + + buildMap { + for (coin in response.coins) { + for (network in coin.networks) { + val address = network.contractAddress?.lowercase() ?: continue + put(address, coin) + } + } + } + } catch (e: Exception) { + TangemLogger.w( + "Failed to fetch catalog info for networkId=$networkId, addresses=${contractAddresses.size}", + e, + ) + emptyMap() + } + } + + private fun getSupportedNetworks(userWalletId: UserWalletId): List { + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) + + return Blockchain.entries + .filter { !it.isTestnet() } + .filter { it !in excludedBlockchains } + .mapNotNull { blockchain -> + networkFactory.create( + blockchain = blockchain, + extraDerivationPath = null, + userWallet = userWallet, + ) + } + } + + private fun getProgressFlow(userWalletId: UserWalletId): MutableStateFlow { + return progressStates.getOrPut(userWalletId.stringValue) { + MutableStateFlow(TokenSyncProgress.Idle) + } + } + + private suspend fun setPendingFlag(userWalletId: UserWalletId, value: Boolean) { + appPreferencesStore.editData { prefs -> + prefs.setObjectMap( + key = PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY, + value = prefs.getObjectMap(PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY) + .plus(userWalletId.stringValue to value), + ) + } + } + + private fun DiscoveredToken.toResponseToken(): UserTokensResponse.Token { + return UserTokensResponse.Token( + id = currencyId, + networkId = networkId, + name = name, + symbol = symbol, + decimals = decimals, + contractAddress = contractAddress, + ) + } + + private data class DiscoveredToken( + val contractAddress: String?, + val symbol: String, + val name: String, + val decimals: Int, + val amount: BigDecimal, + val isNativeToken: Boolean, + val currencyId: String?, + val networkId: String, + ) + + private sealed class NetworkResult { + data class Success( + val networkId: String, + val responseTokens: List, + ) : NetworkResult() + + data class Error( + val networkId: String, + val cause: Throwable, + ) : NetworkResult() + } + + companion object { + private const val MAX_CONCURRENT_REQUESTS = 3 + } +} \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt index e9656e1601..3561434daf 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt @@ -78,6 +78,33 @@ class ManageCryptoCurrenciesUseCase( add: List = emptyList(), remove: List = emptyList(), skipDerivationErrors: Boolean = true, + ): Either = invokeInternal( + accountId = accountId, + add = add, + remove = remove, + skipDerivationErrors = skipDerivationErrors, + awaitTokensSyncFinished = false, + ) + + suspend fun invokeAndAwait( + accountId: AccountId, + add: List = emptyList(), + remove: List = emptyList(), + skipDerivationErrors: Boolean = true, + ): Either = invokeInternal( + accountId = accountId, + add = add, + remove = remove, + skipDerivationErrors = skipDerivationErrors, + awaitTokensSyncFinished = true, + ) + + private suspend fun invokeInternal( + accountId: AccountId, + add: List, + remove: List, + skipDerivationErrors: Boolean, + awaitTokensSyncFinished: Boolean, ): Either = eitherOn(dispatchers.default) { if (add.isEmpty() && remove.isEmpty()) { TangemLogger.d("No currencies to add or remove, skipping") @@ -111,9 +138,11 @@ class ManageCryptoCurrenciesUseCase( account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total), ) - parallelUpdatingScope.launch { - syncTokens(userWalletId, modifiedCurrencyList) - + syncTokensAndLaunchUpdates( + userWalletId = userWalletId, + modifiedCurrencyList = modifiedCurrencyList, + awaitSync = awaitTokensSyncFinished, + ) { cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total) clearMetadata(userWalletId = userWalletId, currencies = modifiedCurrencyList.removed) @@ -129,6 +158,7 @@ class ManageCryptoCurrenciesUseCase( accountId: AccountId, networkId: String, contractAddress: String, + awaitTokensSyncFinished: Boolean = false, ): Either = eitherOn(dispatchers.default) { val userWalletId = accountId.userWalletId @@ -152,9 +182,11 @@ class ManageCryptoCurrenciesUseCase( saveAccount(account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total)) - parallelUpdatingScope.launch { - syncTokens(userWalletId, modifiedCurrencyList) - + syncTokensAndLaunchUpdates( + userWalletId = userWalletId, + modifiedCurrencyList = modifiedCurrencyList, + awaitSync = awaitTokensSyncFinished, + ) { cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = listOf(tokenToAdd)) refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total) } @@ -285,6 +317,25 @@ class ManageCryptoCurrenciesUseCase( .onFailure { TangemLogger.e("Failed to sync tokens for wallet $userWalletId", it) } } + private suspend fun syncTokensAndLaunchUpdates( + userWalletId: UserWalletId, + modifiedCurrencyList: ModifiedCurrencyList, + awaitSync: Boolean, + updates: suspend () -> Unit, + ) { + if (awaitSync) { + syncTokens(userWalletId, modifiedCurrencyList) + parallelUpdatingScope.launch { + updates() + } + } else { + parallelUpdatingScope.launch { + syncTokens(userWalletId, modifiedCurrencyList) + updates() + } + } + } + /** * Creates wallet managers for the given [currencies] if they do not already exist. * The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature. diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/tokensync/DiscoveredToken.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/tokensync/DiscoveredToken.kt deleted file mode 100644 index ddf1a0069b..0000000000 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/tokensync/DiscoveredToken.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.domain.tokens.model.tokensync - -import java.math.BigDecimal - -data class DiscoveredToken( - val contractAddress: String?, - val symbol: String, - val name: String, - val decimals: Int, - val amount: BigDecimal, - val isNativeToken: Boolean, - val currencyId: String?, - val networkId: String, -) \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/tokensync/TokenSyncProgress.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/tokensync/TokenSyncProgress.kt deleted file mode 100644 index 9e94d71268..0000000000 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/tokensync/TokenSyncProgress.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.domain.tokens.model.tokensync - -sealed class TokenSyncProgress { - - data object Idle : TokenSyncProgress() - - data class InProgress( - val completedNetworks: Int, - val totalNetworks: Int, - ) : TokenSyncProgress() { - val progressPercent: Int - get() = if (totalNetworks > 0) { - completedNetworks * 100 / totalNetworks - } else { - 0 - } - } - - data object Completed : TokenSyncProgress() - - data class Error(val cause: Throwable) : TokenSyncProgress() -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokenSyncRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokenSyncRepository.kt deleted file mode 100644 index 5919dd5414..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/TokenSyncRepository.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.domain.tokens.repository - -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.model.tokensync.TokenSyncProgress -import kotlinx.coroutines.flow.Flow - -interface TokenSyncRepository { - - suspend fun runSync(userWalletId: UserWalletId) - - suspend fun getPendingSyncWalletIds(): List - - fun observeSyncProgress(userWalletId: UserWalletId): Flow - - fun acknowledgeCompletion(userWalletId: UserWalletId) - - suspend fun clearPendingFlag(userWalletId: UserWalletId) - - suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List - - suspend fun clearDiscoveredTokens(userWalletId: UserWalletId) -} \ No newline at end of file diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/model/TokenSyncProgress.kt b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/model/TokenSyncProgress.kt index f094aa655a..ee78b42a70 100644 --- a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/model/TokenSyncProgress.kt +++ b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/model/TokenSyncProgress.kt @@ -17,6 +17,4 @@ sealed class TokenSyncProgress { } data object Completed : TokenSyncProgress() - - data class Error(val cause: Throwable) : TokenSyncProgress() } \ No newline at end of file diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/repository/TokenSyncRepository.kt b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/repository/TokenSyncRepository.kt index c6f2d19976..b56da08053 100644 --- a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/repository/TokenSyncRepository.kt +++ b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/repository/TokenSyncRepository.kt @@ -9,6 +9,8 @@ interface TokenSyncRepository { suspend fun runSync(userWalletId: UserWalletId) + suspend fun completeSync(userWalletId: UserWalletId) + suspend fun getPendingSyncWalletIds(): List fun observeSyncProgress(userWalletId: UserWalletId): Flow diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/StartTokenSyncUseCase.kt similarity index 94% rename from domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt rename to domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/StartTokenSyncUseCase.kt index ddf41c5794..d0a1efdfa5 100644 --- a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/SyncTokensUseCase.kt +++ b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/StartTokenSyncUseCase.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import java.util.concurrent.ConcurrentHashMap -class SyncTokensUseCase( +class StartTokenSyncUseCase( private val tokenSyncRepository: TokenSyncRepository, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val appCoroutineScope: AppCoroutineScope, @@ -25,6 +25,7 @@ class SyncTokensUseCase( try { tokenSyncRepository.runSync(userWalletId) applyDiscoveredTokens(userWalletId) + tokenSyncRepository.completeSync(userWalletId) } catch (e: Exception) { TangemLogger.e("Token sync failed for wallet: $userWalletId", e) } finally { @@ -61,7 +62,7 @@ class SyncTokensUseCase( if (currencies.isEmpty()) return true val accountId = AccountId.forMainCryptoPortfolio(userWalletId) - return manageCryptoCurrenciesUseCase( + return manageCryptoCurrenciesUseCase.invokeAndAwait( accountId = accountId, add = currencies, ).fold( diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index 37c0ca6773..0daf9f2d1e 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(projects.domain.feedback) implementation(projects.domain.feedback.models) implementation(projects.domain.hotWallet) + implementation(projects.domain.tokensync) /** Common */ implementation(projects.common.ui) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index f23c52ad2f..c71cf787d8 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -11,10 +11,12 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.CanUseBiometryUseCase +import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_FAST_FORWARD_ATTEMPTS import com.tangem.domain.wallets.hot.HotWalletPasswordRequester +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM import com.tangem.features.hotwallet.impl.R @@ -30,12 +32,15 @@ import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @ModelScoped +@Suppress("LongParameterList") internal class HotAccessCodeRequestModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val hotAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, private val userWalletsListRepository: UserWalletsListRepository, private val canUseBiometryUseCase: CanUseBiometryUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + private val startTokenSyncUseCase: StartTokenSyncUseCase, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val result = MutableStateFlow(null) @@ -214,6 +219,11 @@ internal class HotAccessCodeRequestModel @Inject constructor( val currentRequest = currentRequest.value ?: return val userWallet = userWalletsListRepository.userWalletsSync() .firstOrNull { it is UserWallet.Hot && it.hotWalletId == currentRequest.hotWalletId } ?: return + + if (hotWalletFeatureToggles.isTokenSyncEnabled) { + startTokenSyncUseCase.cancel(userWallet.walletId) + } + userWalletsListRepository.delete(listOf(userWallet.walletId)) dismiss() } diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index eb4f297ea2..ed40d1e958 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -17,8 +17,10 @@ import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.crypto.bip39.Mnemonic import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.common.wallets.error.SaveWalletError +import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase import com.tangem.domain.wallets.builder.HotUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.hotwallet.MnemonicRepository import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM @@ -40,6 +42,8 @@ internal class AddExistingWalletImportModel @Inject constructor( private val tangemHotSdk: TangemHotSdk, private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory, private val saveUserWalletUseCase: SaveWalletUseCase, + private val startTokenSyncUseCase: StartTokenSyncUseCase, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, private val appsFlyerStore: AppsFlyerStore, @@ -109,6 +113,11 @@ internal class AddExistingWalletImportModel @Inject constructor( } .onRight { setImportProgress(false) + + if (hotWalletFeatureToggles.isTokenSyncEnabled) { + startTokenSyncUseCase(userWallet.walletId) + } + analyticsEventHandler.send( event = OnboardingAnalyticsEvent.Onboarding.Finished( source = AnalyticsParam.ScreensSources.ImportWallet.value, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt index 872ba5c072..fa801e998e 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt @@ -12,8 +12,10 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.features.hotwallet.ForgetWalletComponent +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.hotwallet.forgetwallet.entity.ForgetWalletUM import com.tangem.features.hotwallet.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -23,6 +25,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @ModelScoped internal class ForgetWalletModel @Inject constructor( paramsContainer: ParamsContainer, @@ -30,6 +33,8 @@ internal class ForgetWalletModel @Inject constructor( private val router: Router, private val deleteWalletUseCase: DeleteWalletUseCase, private val uiMessageSender: UiMessageSender, + private val startTokenSyncUseCase: StartTokenSyncUseCase, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val params = paramsContainer.require() @@ -79,6 +84,10 @@ internal class ForgetWalletModel @Inject constructor( private fun forgetWallet() { modelScope.launch { + if (hotWalletFeatureToggles.isTokenSyncEnabled) { + startTokenSyncUseCase.cancel(params.userWalletId) + } + val hasUserWallets = deleteWalletUseCase(params.userWalletId) .getOrElse { error -> TangemLogger.e("Unable to delete wallet: $error") diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt index 38034501c5..fcce6daea7 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt @@ -9,7 +9,6 @@ enum class ManageTokensSource(val analyticsName: String) { ONBOARDING(analyticsName = "Onboarding"), SETTINGS(analyticsName = "Wallet Settings"), ACCOUNT(analyticsName = "Account"), - TOKEN_SYNC_BANNER(analyticsName = "Token Sync Banner"), SEND_VIA_SWAP(analyticsName = "SendViaSwap"), } diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index 634724e099..ec188e4f5e 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -50,6 +50,7 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.notifications.models) implementation(projects.domain.notifications) + implementation(projects.domain.tokensync) /* AndroidX */ implementation(deps.androidx.fragment.ktx) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 52efecf7c9..4391d1ca87 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -37,6 +37,7 @@ import com.tangem.domain.nft.EnableWalletNFTUseCase import com.tangem.domain.nft.GetWalletNFTEnabledUseCase import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.settings.repositories.PermissionRepository +import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase import com.tangem.domain.wallets.analytics.Settings import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents.RecoveryPhraseScreenAction @@ -48,6 +49,7 @@ import com.tangem.feature.walletsettings.utils.AccountItemsDelegate import com.tangem.feature.walletsettings.utils.AccountListSortingSaver import com.tangem.feature.walletsettings.utils.ItemsBuilder import com.tangem.feature.walletsettings.utils.WalletCardItemDelegate +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -88,6 +90,8 @@ internal class WalletSettingsModel @Inject constructor( private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val singleAccountListSupplier: SingleAccountListSupplier, private val accountListSortingSaver: AccountListSortingSaver, + private val startTokenSyncUseCase: StartTokenSyncUseCase, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { val params: WalletSettingsComponent.Params = paramsContainer.require() @@ -249,6 +253,13 @@ internal class WalletSettingsModel @Inject constructor( } private fun forgetWallet() = modelScope.launch { + val userWallet = getUserWalletUseCase(params.userWalletId) + .getOrNull() + + if (userWallet is UserWallet.Hot && hotWalletFeatureToggles.isTokenSyncEnabled) { + startTokenSyncUseCase.cancel(params.userWalletId) + } + val hasUserWallets = deleteWalletUseCase(params.userWalletId).getOrElse { error -> TangemLogger.e("Unable to delete wallet: $error") diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index e3f92c0e20..d612025527 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -125,6 +125,7 @@ dependencies { implementation(projects.domain.yieldSupply.models) implementation(projects.domain.appTheme) implementation(projects.domain.appTheme.models) + implementation(projects.domain.tokensync) /** Feature Apis */ implementation(projects.features.details.api) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 16d2963864..d304d3fe84 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -38,6 +38,8 @@ import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.WcPairRequest +import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.domain.wallets.usecase.* import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -123,6 +125,8 @@ internal class WalletModel @Inject constructor( private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val uiMessageSender: UiMessageSender, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val startTokenSyncUseCase: StartTokenSyncUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -155,6 +159,7 @@ internal class WalletModel @Inject constructor( subscribeTangemPayOnWalletState() subscribeToMainScreenQrScanning() enableNotificationsIfNeeded() + applyPendingTokenSyncs() clickIntents.initialize(innerWalletRouter, modelScope) @@ -819,6 +824,12 @@ internal class WalletModel @Inject constructor( } } + private fun applyPendingTokenSyncs() { + if (hotWalletFeatureToggles.isTokenSyncEnabled) { + startTokenSyncUseCase.applyPendingSyncs() + } + } + private fun enableNotificationsIfNeeded() { modelScope.launch { val isUserAllowToEnableNotifications = notificationsRepository.isUserAllowToSubscribeOnPushNotifications() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index ee38dd19e0..665908c863 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -19,6 +19,7 @@ import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.hotwallet.CloseHotWalletUpgradeBannerUseCase +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -39,6 +40,7 @@ import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.PromotionBannerClicked import com.tangem.domain.tokens.model.details.NavigationAction +import com.tangem.domain.tokensync.usecase.AcknowledgeTokenSyncCompletionUseCase import com.tangem.domain.wallets.usecase.* import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic @@ -94,6 +96,10 @@ internal interface WalletWarningsClickIntents { fun onUpgradeHotWalletClick(userWalletId: UserWalletId) fun onCloseUpgradeBannerClick(userWalletId: UserWalletId) + + fun onDismissTokenSyncNotification(userWalletId: UserWalletId) + + fun onTokenSyncManageClick(userWalletId: UserWalletId) } @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @@ -126,6 +132,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val uiMessageSender: UiMessageSender, private val reviewManager: ReviewManager, private val closeHotWalletUpgradeBannerUseCase: CloseHotWalletUpgradeBannerUseCase, + private val acknowledgeTokenSyncCompletionUseCase: AcknowledgeTokenSyncCompletionUseCase, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { @@ -501,6 +508,17 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } + override fun onDismissTokenSyncNotification(userWalletId: UserWalletId) { + acknowledgeTokenSyncCompletionUseCase(userWalletId) + } + + override fun onTokenSyncManageClick(userWalletId: UserWalletId) { + acknowledgeTokenSyncCompletionUseCase(userWalletId) + router.openManageTokensScreen( + AccountId.forMainCryptoPortfolio(userWalletId), + ) + } + private companion object { const val VISA_PROMO_LINK = "https://tangem.com/en/cardwaitlist/?utm_source=tangem-app-banner" + "&utm_medium=banner" + diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt index 59253bc6c3..bd122d0d8d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt @@ -20,7 +20,9 @@ internal object WalletPreviewDataLegacy { balance = "8923,05312312312312312312331231231233432423423424234 $", additionalInfo = WalletAdditionalInfo( hideable = false, - content = TextReference.Str("3 cards • Seed phrase3 cards • Seed phrasephrasephrasephrase"), + content = WalletAdditionalInfo.Content.Text( + TextReference.Str("3 cards • Seed phrase3 cards • Seed phrasephrasephrasephrase"), + ), ), imageResId = R.drawable.ill_wallet2_cards3_120_106, dropDownItems = persistentListOf(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt index 91a230aa1f..aa268b3051 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt @@ -160,7 +160,7 @@ internal object WalletScreenPreviewDataLegacy { title = "Note", additionalInfo = WalletAdditionalInfo( hideable = false, - content = TextReference.Str("Locked"), + content = WalletAdditionalInfo.Content.Text(TextReference.Str("Locked")), ), imageResId = R.drawable.ill_note_btc_120_106, dropDownItems = persistentListOf(), @@ -172,7 +172,7 @@ internal object WalletScreenPreviewDataLegacy { title = "Wallet 1", additionalInfo = WalletAdditionalInfo( hideable = false, - content = TextReference.Str("Seed phrase"), + content = WalletAdditionalInfo.Content.Text(TextReference.Str("Seed phrase")), ), imageResId = R.drawable.ill_wallet2_cards3_120_106, cardCount = 3, 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 fd7ada0172..f0d03bf32a 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 @@ -27,6 +27,9 @@ import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.promo.ShouldShowPromoWalletUseCase import com.tangem.domain.promo.models.PromoId import com.tangem.domain.settings.IsReadyToShowRateAppUseCase +import com.tangem.domain.tokensync.model.TokenSyncProgress +import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase +import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -43,6 +46,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import javax.inject.Inject @@ -61,6 +65,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val shouldShowUpgradeHotWalletBannerUseCase: ShouldShowUpgradeHotWalletBannerUseCase, private val getUpgradeBannerClosureTimestampUseCase: GetUpgradeBannerClosureTimestampUseCase, private val checkHotWalletUpgradeBannerUseCase: CheckHotWalletUpgradeBannerUseCase, + private val observeTokenSyncUseCase: ObserveTokenSyncUseCase, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) { @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod", "CastNullableToNonNullableType") @@ -69,6 +75,12 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val params = SingleAccountStatusListProducer.Params(userWallet.walletId) val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params) + val tokenSyncProgressFlow = if (hotWalletFeatureToggles.isTokenSyncEnabled && userWallet is UserWallet.Hot) { + observeTokenSyncUseCase(userWallet.walletId).distinctUntilChanged() + } else { + flowOf(TokenSyncProgress.Idle) + } + return combine( accountStatusListFlow, isReadyToShowRateAppUseCase().distinctUntilChanged(), @@ -84,6 +96,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( .distinctUntilChanged(), getUpgradeBannerClosureTimestampUseCase(userWallet.walletId) .distinctUntilChanged(), + tokenSyncProgressFlow, ) { array -> array } .map { array -> val accountStatusList = array[0] as AccountStatusList @@ -95,6 +108,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val shouldShowYieldPromo = array[6] as Boolean val shouldShowUpgradeBanner = array[7] as Boolean val closureTimestamp = array[8] as? Long + val tokenSyncProgress = array[9] as TokenSyncProgress val flattenCurrencies = accountStatusList.flattenCurrencies() val paymentAccountStatus = accountStatusList.accountStatuses @@ -139,6 +153,12 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( clickIntents = clickIntents, ) + addTokenSyncCompletedNotification( + userWallet = userWallet, + tokenSyncProgress = tokenSyncProgress, + clickIntents = clickIntents, + ) + addPushReminderNotification( clickIntents = clickIntents, shouldShowPushReminderBanner = shouldShowEnablePushesReminderNotification && @@ -384,6 +404,20 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( // } // } + private fun MutableList.addTokenSyncCompletedNotification( + userWallet: UserWallet, + tokenSyncProgress: TokenSyncProgress, + clickIntents: WalletClickIntents, + ) { + addIf( + element = WalletNotification.TokenSyncCompleted( + onCloseClick = { clickIntents.onDismissTokenSyncNotification(userWallet.walletId) }, + onManageTokensClick = { clickIntents.onTokenSyncManageClick(userWallet.walletId) }, + ), + condition = tokenSyncProgress is TokenSyncProgress.Completed, + ) + } + private fun MutableList.addRateTheAppNotification( isReadyToShowRating: Boolean, clickIntents: WalletClickIntents, 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 b911a9a509..838ea96770 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 @@ -10,6 +10,7 @@ import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.common.util.getCardsCount import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.TokenSyncProgressUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo import java.math.BigDecimal @@ -28,7 +29,11 @@ internal object WalletAdditionalInfoFactory { * @param wallet current wallet * @param currencyAmount amount of currency */ - fun resolve(wallet: UserWallet, currencyAmount: BigDecimal? = null): WalletAdditionalInfo { + fun resolve( + wallet: UserWallet, + currencyAmount: BigDecimal? = null, + syncProgress: TokenSyncProgressUM = TokenSyncProgressUM.Idle, + ): WalletAdditionalInfo { return when (wallet) { is UserWallet.Cold -> { if (wallet.isMultiCurrency) { @@ -37,19 +42,26 @@ internal object WalletAdditionalInfoFactory { wallet.resolveSingleCurrencyInfo(currencyAmount) } } - is UserWallet.Hot -> wallet.resolveAdditionalInfo() + is UserWallet.Hot -> wallet.resolveAdditionalInfo(syncProgress) } } - private fun UserWallet.Hot.resolveAdditionalInfo(): WalletAdditionalInfo { + private fun UserWallet.Hot.resolveAdditionalInfo(syncProgress: TokenSyncProgressUM): WalletAdditionalInfo { + val content = if (syncProgress is TokenSyncProgressUM.InProgress) { + WalletAdditionalInfo.Content.SyncProgress(syncProgress.progressPercent) + } else { + WalletAdditionalInfo.Content.Text( + TextReference.Res(R.string.hw_mobile_wallet) + + when { + isLocked -> DIVIDER + TextReference.Res(R.string.common_locked) + backedUp.not() -> DIVIDER + TextReference.Res(R.string.hw_backup_no_backup) + else -> TextReference.Str("") + }, + ) + } return WalletAdditionalInfo( hideable = false, - content = TextReference.Res(R.string.hw_mobile_wallet) + - when { - isLocked -> DIVIDER + TextReference.Res(R.string.common_locked) - backedUp.not() -> DIVIDER + TextReference.Res(R.string.hw_backup_no_backup) - else -> TextReference.Str("") - }, + content = content, isHotBackedUp = backedUp, ) } @@ -58,9 +70,11 @@ internal object WalletAdditionalInfoFactory { return if (isLocked) { WalletAdditionalInfo( hideable = false, - content = getBackupInfoWithDivider( - backupCardsCount = getCardsCount(), - ) + TextReference.Res(R.string.common_locked), + content = WalletAdditionalInfo.Content.Text( + getBackupInfoWithDivider( + backupCardsCount = getCardsCount(), + ) + TextReference.Res(R.string.common_locked), + ), ) } else { val cardTypeResolver = scanResponse.cardTypesResolver @@ -76,8 +90,10 @@ internal object WalletAdditionalInfoFactory { return if (isImported) { WalletAdditionalInfo( hideable = false, - content = getBackupInfoWithDivider(backupCardsCount = getCardsCount()) + TextReference.Res( - id = R.string.common_seed_phrase, + content = WalletAdditionalInfo.Content.Text( + getBackupInfoWithDivider(backupCardsCount = getCardsCount()) + TextReference.Res( + id = R.string.common_seed_phrase, + ), ), ) } else { @@ -94,7 +110,7 @@ internal object WalletAdditionalInfoFactory { } private fun getBackupInfo(backupCardsCount: Int?): WalletAdditionalInfo { - val content = if (backupCardsCount != null) { + val ref = if (backupCardsCount != null) { getBackupInfoTextReference(count = backupCardsCount) } else { TextReference.EMPTY @@ -102,7 +118,7 @@ internal object WalletAdditionalInfoFactory { return WalletAdditionalInfo( hideable = false, - content = content, + content = WalletAdditionalInfo.Content.Text(ref), ) } @@ -118,7 +134,7 @@ internal object WalletAdditionalInfoFactory { return if (isLocked) { WalletAdditionalInfo( hideable = false, - content = TextReference.Res(R.string.common_locked), + content = WalletAdditionalInfo.Content.Text(TextReference.Res(R.string.common_locked)), ) } else { val blockchain = scanResponse.cardTypesResolver.getBlockchain() @@ -126,7 +142,7 @@ internal object WalletAdditionalInfoFactory { WalletAdditionalInfo( hideable = true, - content = TextReference.Str(value = amount.orEmpty()), + content = WalletAdditionalInfo.Content.Text(TextReference.Str(value = amount.orEmpty())), ) } } 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 69217500ac..4313751a77 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 @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.core.ui.DesignFeatureToggles import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.subscribers.* +import com.tangem.features.hotwallet.HotWalletFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -17,10 +18,12 @@ internal class MultiWalletContentLoader @AssistedInject constructor( private val walletNotificationsSubscriberFactory: WalletNotificationsSubscriber.Factory, private val multiWalletActionButtonsSubscriberFactory: MultiWalletActionButtonsSubscriber.Factory, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, + private val tokenSyncSubscriberFactory: TokenSyncSubscriber.Factory, private val designFeatureToggles: DesignFeatureToggles, + private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : WalletContentLoader(id = userWallet.walletId) { - override fun create(): List = listOf( + override fun create(): List = listOfNotNull( accountListSubscriberFactory.create(userWallet), walletNFTListSubscriberFactory.create(userWallet), checkWalletWithFundsSubscriberFactory.create(userWallet), @@ -31,6 +34,11 @@ internal class MultiWalletContentLoader @AssistedInject constructor( }, multiWalletActionButtonsSubscriberFactory.create(userWallet), tangemPayMainSubscriberFactory.create(userWallet), + if (hotWalletFeatureToggles.isTokenSyncEnabled && userWallet is UserWallet.Hot) { + tokenSyncSubscriberFactory.create(userWallet) + } else { + null + }, ) @AssistedFactory diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenSyncProgressUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenSyncProgressUM.kt new file mode 100644 index 0000000000..2bcd1e9487 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenSyncProgressUM.kt @@ -0,0 +1,13 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.compose.runtime.Immutable + +@Immutable +internal sealed class TokenSyncProgressUM { + + data object Idle : TokenSyncProgressUM() + + data class InProgress(val progressPercent: Int) : TokenSyncProgressUM() + + data object Completed : TokenSyncProgressUM() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAdditionalInfo.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAdditionalInfo.kt index c18257cf76..cc7b235d4e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAdditionalInfo.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAdditionalInfo.kt @@ -6,7 +6,12 @@ import com.tangem.core.ui.extensions.TextReference @Immutable data class WalletAdditionalInfo( val hideable: Boolean, - val content: TextReference, + val content: Content, val isHotBackedUp: Boolean = false, - val shouldShowProgress: Boolean = false, -) \ No newline at end of file +) { + @Immutable + sealed interface Content { + data class Text(val text: TextReference) : Content + data class SyncProgress(val progressPercent: Int) : Content + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt index 52efcdeb0e..1d657b0170 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletCardState.kt @@ -85,7 +85,10 @@ internal sealed interface WalletCardState { private companion object { val defaultAdditionalInfo: WalletAdditionalInfo - get() = WalletAdditionalInfo(hideable = true, content = EMPTY_BALANCE_TEXT) + get() = WalletAdditionalInfo( + hideable = true, + content = WalletAdditionalInfo.Content.Text(EMPTY_BALANCE_TEXT), + ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 9459402f44..3903da422a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -27,6 +27,7 @@ internal sealed interface WalletState : WalletStateHolder { abstract val tangemPayState: TangemPayState abstract val tangemPayMainUM: TangemPayMainUM abstract val isTangemPayRefactorEnabled: Boolean // TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED + abstract val tokenSyncProgressUM: TokenSyncProgressUM data class Content( override val pullToRefreshConfig: PullToRefreshConfig, @@ -40,6 +41,7 @@ internal sealed interface WalletState : WalletStateHolder { override val tangemPayState: TangemPayState, override val tangemPayMainUM: TangemPayMainUM, override val isTangemPayRefactorEnabled: Boolean, + override val tokenSyncProgressUM: TokenSyncProgressUM = TokenSyncProgressUM.Idle, ) : MultiCurrency() data class Locked( @@ -61,6 +63,7 @@ internal sealed interface WalletState : WalletStateHolder { override val tangemPayState: TangemPayState = TangemPayState.Empty override val tangemPayMainUM: TangemPayMainUM = TangemPayMainUM.Empty override val isTangemPayRefactorEnabled: Boolean = false + override val tokenSyncProgressUM: TokenSyncProgressUM = TokenSyncProgressUM.Idle } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index 6bfbef7ab9..ef5698e378 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -61,7 +61,7 @@ internal class SetTokenListErrorTransformer( walletsBalanceUM = walletUM.walletsBalanceUM.toLoadedState(), tokensListUM = WalletTokensListUM.Empty( onEmptyClick = { - clickIntents.onManageTokensClick(walletUM.walletsBalanceUM.id) + clickIntents.onTokenSyncManageClick(walletUM.walletsBalanceUM.id) }, ), buttons = walletUM.disableButtons(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 27046856a0..ebfe4ac052 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -122,7 +122,7 @@ internal class SetTokenListTransformer( if (params !is TokenConverterParams.Account) { return WalletTokensListUM.Empty( onEmptyClick = { - clickIntents.onManageTokensClick(userWallet.walletId) + clickIntents.onTokenSyncManageClick(userWallet.walletId) }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenSyncProgressTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenSyncProgressTransformer.kt index 283537ff7a..0dfbaf6dd9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenSyncProgressTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenSyncProgressTransformer.kt @@ -1,51 +1,34 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory +import com.tangem.feature.wallet.presentation.wallet.state.model.TokenSyncProgressUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class SetTokenSyncProgressTransformer( - userWalletId: UserWalletId, - private val progressPercent: Int, -) : WalletStateTransformer(userWalletId) { + private val userWallet: UserWallet, + private val progress: TokenSyncProgressUM, +) : WalletStateTransformer(userWallet.walletId) { override fun transform(prevState: WalletState): WalletState { return when (prevState) { - is WalletState.MultiCurrency.Content -> { - val updatedCardState = updateCardState(prevState.walletCardState) - prevState.copy(walletCardState = updatedCardState) - } - else -> { - prevState - } + is WalletState.MultiCurrency.Content -> prevState.copy( + walletCardState = updateCardState(prevState.walletCardState), + tokenSyncProgressUM = progress, + ) + else -> prevState } } - override fun transform(walletUM: WalletUM): WalletUM { - return walletUM - } + override fun transform(walletUM: WalletUM): WalletUM = walletUM private fun updateCardState(cardState: WalletCardState): WalletCardState { - val additionalInfo = WalletAdditionalInfo( - hideable = false, - content = resourceReference( - id = R.string.initial_wallet_sync_restore_progress, - formatArgs = wrappedList(progressPercent), - ), - shouldShowProgress = true, - ) + val additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet, syncProgress = progress) return when (cardState) { - is WalletCardState.Loading -> { - cardState.copy(additionalInfo = additionalInfo) - } - is WalletCardState.Content -> { - cardState.copy(additionalInfo = additionalInfo) - } + is WalletCardState.Loading -> cardState.copy(additionalInfo = additionalInfo) + is WalletCardState.Content -> cardState.copy(additionalInfo = additionalInfo) else -> cardState } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt index 2b4dd9e457..86cc55373e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.domain.card.common.util.getCardsCount import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.state.model.TokenSyncProgressUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM @@ -17,7 +18,9 @@ internal class UpdateWalletCardsCountTransformer( override fun transform(prevState: WalletState): WalletState { return when (prevState) { is WalletState.MultiCurrency.Content -> { - prevState.copy(walletCardState = prevState.walletCardState.toUpdatedState()) + prevState.copy( + walletCardState = prevState.walletCardState.toUpdatedState(prevState.tokenSyncProgressUM), + ) } is WalletState.SingleCurrency.Content -> { prevState.copy(walletCardState = prevState.walletCardState.toUpdatedState()) @@ -35,10 +38,12 @@ internal class UpdateWalletCardsCountTransformer( return walletUM // todo redesign main } - private fun WalletCardState.toUpdatedState(): WalletCardState { + private fun WalletCardState.toUpdatedState( + syncProgress: TokenSyncProgressUM = TokenSyncProgressUM.Idle, + ): WalletCardState { return when (this) { is WalletCardState.Content -> copy( - additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet), + additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet, syncProgress = syncProgress), imageResId = walletImageResolver.resolve(userWallet = userWallet), cardCount = (userWallet as? UserWallet.Cold)?.getCardsCount(), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenSyncSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenSyncSubscriber.kt new file mode 100644 index 0000000000..1095805674 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenSyncSubscriber.kt @@ -0,0 +1,45 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.tokensync.model.TokenSyncProgress +import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.model.TokenSyncProgressUM +import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenSyncProgressTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.onEach + +internal class TokenSyncSubscriber @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val stateController: WalletStateController, + private val observeTokenSyncUseCase: ObserveTokenSyncUseCase, +) : WalletSubscriber() { + + override fun create(coroutineScope: CoroutineScope): Flow<*> { + return observeTokenSyncUseCase(userWallet.walletId) + .onEach { current -> handleProgress(userWallet, current) } + } + + private fun handleProgress(userWallet: UserWallet, current: TokenSyncProgress) { + val progressUM = when (current) { + is TokenSyncProgress.InProgress -> TokenSyncProgressUM.InProgress(current.progressPercent) + is TokenSyncProgress.Completed -> TokenSyncProgressUM.Completed + is TokenSyncProgress.Idle -> TokenSyncProgressUM.Idle + } + stateController.update( + SetTokenSyncProgressTransformer( + userWallet = userWallet, + progress = progressUM, + ), + ) + } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): TokenSyncSubscriber + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 26547001a4..9f7899e8e9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -41,6 +41,9 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.feature.wallet.impl.R import com.tangem.core.ui.res.TangemDimens import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -156,16 +159,10 @@ private fun CardContainer(state: WalletCardState, isBalanceHidden: Boolean, item .padding(vertical = TangemTheme.dimens.spacing8), ) - val additionalText by remember(state.additionalInfo, isBalanceHidden) { - mutableStateOf( - state.additionalInfo?.content?.orMaskWithStars( - maskWithStars = state.additionalInfo?.hideable == true && isBalanceHidden, - ), - ) - } AdditionalInfo( - text = additionalText, - showProgress = state.additionalInfo?.shouldShowProgress == true, + content = state.additionalInfo?.content, + hideable = state.additionalInfo?.hideable == true, + isBalanceHidden = isBalanceHidden, modifier = Modifier.conditional( state.imageResId == null, ) { fillMaxWidth() }, @@ -300,28 +297,53 @@ private fun Modifier.nonContentBalanceSize(dimens: TangemDimens): Modifier { } @Composable -private fun AdditionalInfo(text: TextReference?, showProgress: Boolean, modifier: Modifier = Modifier) { +private fun AdditionalInfo( + content: WalletAdditionalInfo.Content?, + hideable: Boolean, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { AnimatedContent( - targetState = text, + targetState = content, + contentKey = { con -> + when (con) { + is WalletAdditionalInfo.Content.Text -> con + is WalletAdditionalInfo.Content.SyncProgress -> WalletAdditionalInfo.Content.SyncProgress::class + null -> null + } + }, label = "Update the additional text", modifier = modifier, transitionSpec = { fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) togetherWith fadeOut(animationSpec = tween(durationMillis = 90)) }, - ) { animatedText -> - if (animatedText != null) { + ) { animatedContent -> + if (animatedContent != null) { Row( horizontalArrangement = Arrangement.spacedBy(6.dp), ) { - AdditionalInfoText(text = animatedText) - if (showProgress) { - CircularProgressIndicator( - modifier = Modifier - .size(TangemTheme.dimens.size16), - color = TangemTheme.colors.icon.accent, - strokeWidth = TangemTheme.dimens.size2, - ) + when (animatedContent) { + is WalletAdditionalInfo.Content.Text -> { + AdditionalInfoText( + text = animatedContent.text.orMaskWithStars( + maskWithStars = hideable && isBalanceHidden, + ), + ) + } + is WalletAdditionalInfo.Content.SyncProgress -> { + AdditionalInfoText( + text = resourceReference( + id = R.string.initial_wallet_sync_restore_progress, + formatArgs = wrappedList(animatedContent.progressPercent), + ), + ) + CircularProgressIndicator( + modifier = Modifier.size(TangemTheme.dimens.size16), + color = TangemTheme.colors.icon.accent, + strokeWidth = TangemTheme.dimens.size2, + ) + } } } } else { @@ -396,7 +418,7 @@ private class WalletCardStateProvider : CollectionPreviewParameterProvider Date: Wed, 1 Apr 2026 19:03:51 +0500 Subject: [PATCH 52/75] Updated on 2026-08-14 --- .../feature/swap/converters/AccountTokenItemConverter.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt index b28d71800a..0bb771929f 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt @@ -37,7 +37,9 @@ internal class AccountTokenItemConverter( return TokensListPortfolioItemConverter( tokenItemUM = AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, - account = value.account, + account = value.account.copy( + cryptoCurrencies = value.currencyList.map { it.cryptoCurrencyStatus.currency }, + ), onItemClick = onAccountItemClick, ).convert( TotalFiatBalance.Loaded( From 85f0e5858d242b15f30d88a8722d19c93f0b2af7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 1 Apr 2026 14:17:36 +0000 Subject: [PATCH 53/75] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 08ab7327d8..ebcd3e5e91 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,11 +5,11 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.36-1470" +tangemBlockchainSdk = "develop-1461" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.36-600" +tangemCardSdk = "develop-598" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ -tangemVico = "2.0.0-alpha.25-tangem12" +tangemVico = "tangem-master-21" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ tangemHotSdk = "develop-549" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 8c8e00e7b4257916ba8bac1e77f3df473ad67882 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 1 Apr 2026 17:41:23 +0300 Subject: [PATCH 54/75] Updated on 2026-08-14 --- .../core/ui/ds/tabs/TangemSegmentedPicker.kt | 44 ++++++++++--------- .../ui/ds/tabs/TangemSegmentedPickerUM.kt | 2 - .../tangem/core/ui/res/TangemThemeRedesign.kt | 2 +- .../detailed/MarketsTokenDetailsContent.kt | 24 +++++++++- .../detailed/components/InsightsBlock.kt | 1 - .../page/tabs/TangemSegmentedPickerStory.kt | 8 +--- 6 files changed, 50 insertions(+), 31 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemSegmentedPicker.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemSegmentedPicker.kt index 3f98cea45a..b685eab6d8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemSegmentedPicker.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemSegmentedPicker.kt @@ -2,6 +2,7 @@ package com.tangem.core.ui.ds.tabs import android.content.res.Configuration import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -13,6 +14,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.layout.onGloballyPositioned @@ -52,7 +54,6 @@ fun TangemSegmentedPicker( items = tangemSegmentedPickerUM.items, modifier = modifier, initialSelectedItem = tangemSegmentedPickerUM.initialSelectedItem, - hasSeparator = tangemSegmentedPickerUM.hasSeparator, isFixed = tangemSegmentedPickerUM.isFixed, isAltSurface = tangemSegmentedPickerUM.isAltSurface, onClick = onClick, @@ -66,7 +67,6 @@ fun TangemSegmentedPicker( * @param items List of TangemSegmentUM representing the segments in the picker. * @param modifier Modifier to be applied to the segmented picker. * @param initialSelectedItem Optional TangemSegmentUM representing the initially selected segment. - * @param hasSeparator Boolean indicating whether there is a separator between segments. * @param isFixed Boolean indicating whether the picker has a fixed width. * @param isAltSurface Boolean indicating whether to use an alternative surface style. * @param onClick Lambda function to be invoked when a segment is clicked. @@ -76,7 +76,6 @@ fun TangemSegmentedPicker( items: ImmutableList, modifier: Modifier = Modifier, initialSelectedItem: TangemSegmentUM? = null, - hasSeparator: Boolean = false, isFixed: Boolean = false, isAltSurface: Boolean = false, minSegmentWidth: Dp = Dp.Unspecified, @@ -91,11 +90,6 @@ fun TangemSegmentedPicker( val segmentHeight = remember { mutableStateOf(0.dp) } val shape = RoundedCornerShape(TangemTheme.dimens2.x25) - val spacing = if (hasSeparator) { - TangemTheme.dimens2.x4 - } else { - TangemTheme.dimens2.x1 - } Box( modifier = modifier @@ -113,12 +107,8 @@ fun TangemSegmentedPicker( itemsWidths = itemsWidths, selectedIndex = selectedIndex.value, segmentHeight = segmentHeight.value, - spacing = spacing, ) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(spacing), - ) { + Row(verticalAlignment = Alignment.CenterVertically) { items.fastForEachIndexed { index, item -> Segment( item = item, @@ -135,15 +125,34 @@ fun TangemSegmentedPicker( } }, ) + + if (index != items.lastIndex) { + val selected by selectedIndex + val alpha by animateFloatAsState( + targetValue = if (selected == index || selected == index + 1) 0f else 1f, + animationSpec = tween(durationMillis = 300), + label = "separatorAlpha", + ) + + Box( + Modifier + .alpha(alpha) + .width(0.5.dp) + .height(20.dp) + .background( + color = TangemTheme.colors2.border.neutral.tertiary.copy(alpha = 0.1f), + ), + ) + } } } } } @Composable -private fun SegmentSelection(itemsWidths: SnapshotStateList, selectedIndex: Int, segmentHeight: Dp, spacing: Dp) { +private fun SegmentSelection(itemsWidths: SnapshotStateList, selectedIndex: Int, segmentHeight: Dp) { val indicatorOffset by animateDpAsState( - targetValue = itemsWidths.take(selectedIndex).fold(0.dp, Dp::plus) + spacing * selectedIndex, + targetValue = itemsWidths.take(selectedIndex).fold(0.dp, Dp::plus), animationSpec = tween(durationMillis = 300), label = "indicatorOffset", ) @@ -228,7 +237,6 @@ private fun TangemSegmentedPicker_Preview(@PreviewParameter(PreviewProvider::cla ) { TangemSegmentedPicker( isFixed = params.isFixed, - hasSeparator = params.hasSeparator, isAltSurface = params.isAltSurface, items = params.items, initialSelectedItem = params.items.last(), @@ -248,25 +256,21 @@ private class PreviewProvider : PreviewParameterProvider, val initialSelectedItem: TangemSegmentUM? = null, - val hasSeparator: Boolean = false, val isFixed: Boolean = false, val isAltSurface: Boolean = false, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt index adac73f3c7..8428d0f240 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt @@ -94,7 +94,7 @@ private fun lightThemeColors2(): TangemColors2 { neutral = TangemColors2.Border.Neutral( primary = TangemColorPalette.Light3, secondary = TangemColorPalette.Light5, - tertiary = TangemColorPalette.Light_10, + tertiary = TangemColorPalette.Dark_10, quaternary = TangemColorPalette.Dark_10, ), status = TangemColors2.Border.Status( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt index eb0862ce2e..8cbb7fb5e1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt @@ -32,6 +32,8 @@ import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons import com.tangem.core.ui.components.currency.icon.CoinIcon import com.tangem.core.ui.components.marketprice.PriceChangeInPercent import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.ds.tabs.TangemSegmentUM +import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference @@ -49,6 +51,7 @@ import com.tangem.features.feed.ui.market.detailed.state.InfoBottomSheetContent import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreBottomSheetContent import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.distinctUntilChanged import com.tangem.core.ui.R as CoreR @@ -245,6 +248,26 @@ private fun IntervalSelector( onIntervalClick: (PriceChangeInterval) -> Unit, modifier: Modifier = Modifier, ) { + if (LocalRedesignEnabled.current) { + val items = remember { + PriceChangeInterval.entries + .map { TangemSegmentUM(it.toString(), it.getText()) }.toImmutableList() + } + val selectedItem = remember(trendInterval) { + items.firstOrNull { it.id == trendInterval.toString() } + } + + TangemSegmentedPicker( + items = items, + initialSelectedItem = selectedItem, + isFixed = true, + modifier = modifier, + onClick = { onIntervalClick(PriceChangeInterval.valueOf(it.id)) }, + ) + + return + } + SegmentedButtons( config = persistentListOf( PriceChangeInterval.H24, @@ -296,7 +319,6 @@ private fun ShowPriceSubtitleEffect(lazyListState: LazyListState, onShouldShowPr } } -@Composable fun PriceChangeInterval.getText(): TextReference { return when (this) { PriceChangeInterval.H24 -> resourceReference(R.string.markets_selector_interval_24h_title) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/InsightsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/InsightsBlock.kt index 18c97a1f39..0d0dc27c6a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/InsightsBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/InsightsBlock.kt @@ -146,7 +146,6 @@ private fun InsightsBlockV2(state: InsightsUM, modifier: Modifier = Modifier) { TangemSegmentedPicker( items = segmentItems, initialSelectedItem = segmentItems.first(), - hasSeparator = true, isFixed = false, isAltSurface = true, minSegmentWidth = 48.dp, diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/TangemSegmentedPickerStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/TangemSegmentedPickerStory.kt index df03dcb6d4..ac87504551 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/TangemSegmentedPickerStory.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/TangemSegmentedPickerStory.kt @@ -45,15 +45,12 @@ private val items5 = persistentListOf( private data class PickerConfig( val label: String, - val hasSeparator: Boolean, val isFixed: Boolean, ) private val configs = listOf( - PickerConfig("Default", hasSeparator = false, isFixed = false), - PickerConfig("Separator", hasSeparator = true, isFixed = false), - PickerConfig("Fixed", hasSeparator = false, isFixed = true), - PickerConfig("Fixed + Separator", hasSeparator = true, isFixed = true), + PickerConfig("Default", isFixed = false), + PickerConfig("Fixed", isFixed = true), ) @Composable @@ -119,7 +116,6 @@ private fun PickerRow(config: PickerConfig, isAltSurface: Boolean) { ) TangemSegmentedPicker( items = items4, - hasSeparator = config.hasSeparator, isFixed = config.isFixed, isAltSurface = isAltSurface, onClick = {}, From 95b6933a147b0cb082aeb5d8beff199ed1906fdc Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 1 Apr 2026 18:44:35 +0300 Subject: [PATCH 55/75] Updated on 2026-08-14 --- .../tokens/DefaultTokensFeatureToggles.kt | 8 ++--- domain/dynamic-addresses/build.gradle.kts | 20 +++++++++++ .../dynamic-addresses/models/build.gradle.kts | 7 ++++ .../model/ConsolidationInfo.kt | 9 +++++ .../model/DynamicAddressesStatus.kt | 18 ++++++++++ .../DisableDynamicAddressesUseCase.kt | 33 +++++++++++++++++++ .../DynamicAddressesFeatureToggles.kt | 6 ++++ .../EnableDynamicAddressesUseCase.kt | 16 +++++++++ .../GetConsolidationInfoUseCase.kt | 16 +++++++++ .../GetDynamicAddressesStatusUseCase.kt | 16 +++++++++ .../GetDynamicReceiveAddressUseCase.kt | 16 +++++++++ .../repository/ConsolidationRepository.kt | 21 ++++++++++++ .../repository/DynamicAddressesRepository.kt | 22 +++++++++++++ .../domain/tokens/TokensFeatureToggles.kt | 4 +-- settings.gradle.kts | 2 ++ 15 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 domain/dynamic-addresses/build.gradle.kts create mode 100644 domain/dynamic-addresses/models/build.gradle.kts create mode 100644 domain/dynamic-addresses/models/src/main/kotlin/com/tangem/domain/dynamicaddresses/model/ConsolidationInfo.kt create mode 100644 domain/dynamic-addresses/models/src/main/kotlin/com/tangem/domain/dynamicaddresses/model/DynamicAddressesStatus.kt create mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DisableDynamicAddressesUseCase.kt create mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesFeatureToggles.kt create mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/EnableDynamicAddressesUseCase.kt create mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetConsolidationInfoUseCase.kt create mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetDynamicAddressesStatusUseCase.kt create mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetDynamicReceiveAddressUseCase.kt create mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/ConsolidationRepository.kt create mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt index a223acdcac..0029654a9b 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt @@ -1,13 +1,9 @@ package com.tangem.tap.domain.tokens -import com.tangem.core.configtoggle.FeatureToggles import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.tokens.TokensFeatureToggles +@Suppress("UnusedPrivateProperty") internal class DefaultTokensFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, -) : TokensFeatureToggles { - - override val isDynamicAddressesEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.DYNAMIC_ADDRESSES_ENABLED) -} \ No newline at end of file +) : TokensFeatureToggles \ No newline at end of file diff --git a/domain/dynamic-addresses/build.gradle.kts b/domain/dynamic-addresses/build.gradle.kts new file mode 100644 index 0000000000..6190ff052f --- /dev/null +++ b/domain/dynamic-addresses/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.domain.dynamicaddresses" +} + +dependencies { + api(projects.domain.core) + api(projects.domain.dynamicAddresses.models) + + implementation(projects.domain.models) + + implementation(tangemDeps.blockchain) { + exclude(module = "joda-time") + } +} \ No newline at end of file diff --git a/domain/dynamic-addresses/models/build.gradle.kts b/domain/dynamic-addresses/models/build.gradle.kts new file mode 100644 index 0000000000..64e9c3fdfe --- /dev/null +++ b/domain/dynamic-addresses/models/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +dependencies { +} \ No newline at end of file diff --git a/domain/dynamic-addresses/models/src/main/kotlin/com/tangem/domain/dynamicaddresses/model/ConsolidationInfo.kt b/domain/dynamic-addresses/models/src/main/kotlin/com/tangem/domain/dynamicaddresses/model/ConsolidationInfo.kt new file mode 100644 index 0000000000..47c36bd8f9 --- /dev/null +++ b/domain/dynamic-addresses/models/src/main/kotlin/com/tangem/domain/dynamicaddresses/model/ConsolidationInfo.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.dynamicaddresses.model + +import java.math.BigDecimal + +data class ConsolidationInfo( + val fee: BigDecimal, + val inputCount: Int, + val canCoverFee: Boolean, +) \ No newline at end of file diff --git a/domain/dynamic-addresses/models/src/main/kotlin/com/tangem/domain/dynamicaddresses/model/DynamicAddressesStatus.kt b/domain/dynamic-addresses/models/src/main/kotlin/com/tangem/domain/dynamicaddresses/model/DynamicAddressesStatus.kt new file mode 100644 index 0000000000..a64a721595 --- /dev/null +++ b/domain/dynamic-addresses/models/src/main/kotlin/com/tangem/domain/dynamicaddresses/model/DynamicAddressesStatus.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.dynamicaddresses.model + +import java.math.BigDecimal + +enum class DynamicAddressesStatus { + ENABLED, + + DISABLED, + + /** Enabled on backend, but local XPUB setup required (cross-device sync) */ + ENABLED_REQUIRES_SETUP, +} + +data class UsedAddress( + val address: String, + val path: String, + val balance: BigDecimal, +) \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DisableDynamicAddressesUseCase.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DisableDynamicAddressesUseCase.kt new file mode 100644 index 0000000000..ff9e869754 --- /dev/null +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DisableDynamicAddressesUseCase.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.dynamicaddresses + +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.either +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.dynamicaddresses.model.ConsolidationInfo +import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository +import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository + +class DisableDynamicAddressesUseCase( + private val dynamicAddressesRepository: DynamicAddressesRepository, + private val consolidationRepository: ConsolidationRepository, +) { + + /** + * Returns [ConsolidationInfo] when consolidation is required before disabling, + * or null when DA can be disabled immediately (no non-base balances). + */ + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either = + either { + val hasNonBaseBalances = dynamicAddressesRepository.hasNonBaseBalances(userWalletId, network) + + if (!hasNonBaseBalances) { + dynamicAddressesRepository.disable(userWalletId, network) + return@either null + } + + consolidationRepository.getConsolidationInfo(userWalletId, network) + .getOrElse { raise(it) } + } +} \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesFeatureToggles.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesFeatureToggles.kt new file mode 100644 index 0000000000..0d665d090e --- /dev/null +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesFeatureToggles.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.dynamicaddresses + +interface DynamicAddressesFeatureToggles { + + val isDynamicAddressesEnabled: Boolean +} \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/EnableDynamicAddressesUseCase.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/EnableDynamicAddressesUseCase.kt new file mode 100644 index 0000000000..8b888a54f4 --- /dev/null +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/EnableDynamicAddressesUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.dynamicaddresses + +import arrow.core.Either +import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId + +class EnableDynamicAddressesUseCase( + private val dynamicAddressesRepository: DynamicAddressesRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, network: Network, xpub: String): Either = + Either.catch { + dynamicAddressesRepository.enable(userWalletId, network, xpub) + } +} \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetConsolidationInfoUseCase.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetConsolidationInfoUseCase.kt new file mode 100644 index 0000000000..a498b28cc7 --- /dev/null +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetConsolidationInfoUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.dynamicaddresses + +import arrow.core.Either +import com.tangem.domain.dynamicaddresses.model.ConsolidationInfo +import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId + +class GetConsolidationInfoUseCase( + private val consolidationRepository: ConsolidationRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either { + return consolidationRepository.getConsolidationInfo(userWalletId, network) + } +} \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetDynamicAddressesStatusUseCase.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetDynamicAddressesStatusUseCase.kt new file mode 100644 index 0000000000..3ea5c97bdc --- /dev/null +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetDynamicAddressesStatusUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.dynamicaddresses + +import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus +import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow + +class GetDynamicAddressesStatusUseCase( + private val dynamicAddressesRepository: DynamicAddressesRepository, +) { + + operator fun invoke(userWalletId: UserWalletId, network: Network): Flow { + return dynamicAddressesRepository.getStatus(userWalletId, network) + } +} \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetDynamicReceiveAddressUseCase.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetDynamicReceiveAddressUseCase.kt new file mode 100644 index 0000000000..591d915366 --- /dev/null +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetDynamicReceiveAddressUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.dynamicaddresses + +import arrow.core.Either +import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId + +class GetDynamicReceiveAddressUseCase( + private val dynamicAddressesRepository: DynamicAddressesRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either = + Either.catch { + dynamicAddressesRepository.getReceiveAddress(userWalletId, network) + } +} \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/ConsolidationRepository.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/ConsolidationRepository.kt new file mode 100644 index 0000000000..4eaa27eb38 --- /dev/null +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/ConsolidationRepository.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.dynamicaddresses.repository + +import arrow.core.Either +import com.tangem.blockchain.common.TransactionSigner +import com.tangem.domain.dynamicaddresses.model.ConsolidationInfo +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId + +interface ConsolidationRepository { + + suspend fun getConsolidationInfo( + userWalletId: UserWalletId, + network: Network, + ): Either + + suspend fun sendConsolidationTransaction( + userWalletId: UserWalletId, + network: Network, + signer: TransactionSigner, + ): Either +} \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt new file mode 100644 index 0000000000..c30355fa48 --- /dev/null +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.dynamicaddresses.repository + +import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow + +interface DynamicAddressesRepository { + + fun getStatus(userWalletId: UserWalletId, network: Network): Flow + + suspend fun enable(userWalletId: UserWalletId, network: Network, xpub: String) + + suspend fun disable(userWalletId: UserWalletId, network: Network) + + suspend fun getReceiveAddress(userWalletId: UserWalletId, network: Network): String + + // for explorer url + suspend fun getLastUsedReceiveAddress(userWalletId: UserWalletId, network: Network): String? + + suspend fun hasNonBaseBalances(userWalletId: UserWalletId, network: Network): Boolean +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt index ea588e781c..a2eeb1e0a6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt @@ -5,6 +5,4 @@ package com.tangem.domain.tokens * [REDACTED_AUTHOR] */ -interface TokensFeatureToggles { - val isDynamicAddressesEnabled: Boolean -} \ No newline at end of file +interface TokensFeatureToggles \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 46f42fb5bc..6131532e14 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -335,6 +335,8 @@ include(":domain:common") include(":domain:core") include(":domain:demo") include(":domain:demo:models") +include(":domain:dynamic-addresses") +include(":domain:dynamic-addresses:models") include(":domain:settings") include(":domain:tokens") include(":domain:tokens:models") From bb2877e36caebb9418b03b96801cbdaa6568c63c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Apr 2026 11:22:06 +0400 Subject: [PATCH 56/75] Updated on 2026-08-14 --- .../usecase/ValidateWalletAddressUseCase.kt | 19 +- .../ValidateWalletAddressUseCaseTest.kt | 207 ++++++++++++++++++ .../SendDestinationComponentParams.kt | 3 + .../destination/model/SendDestinationModel.kt | 1 + .../DefaultSendWithSwapComponent.kt | 1 + .../confirm/SendWithSwapConfirmComponent.kt | 1 + 6 files changed, 225 insertions(+), 7 deletions(-) create mode 100644 domain/transaction/src/test/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCaseTest.kt diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt index 88413f9891..ab8e62b026 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCase.kt @@ -26,10 +26,12 @@ class ValidateWalletAddressUseCase( network: Network, address: String, currencyAddresses: Set?, + allowSelfSend: Boolean = false, ): AddressValidationResult = validateAddressInternal( - userWalletId, - network, - address, + userWalletId = userWalletId, + network = network, + address = address, + allowSelfSend = allowSelfSend, isCurrentAddress = { toValidate -> currencyAddresses?.any { it.value == toValidate } ?: true }, @@ -40,10 +42,12 @@ class ValidateWalletAddressUseCase( network: Network, address: String, senderAddresses: List, + allowSelfSend: Boolean = false, ): AddressValidationResult = validateAddressInternal( - userWalletId, - network, - address, + userWalletId = userWalletId, + network = network, + address = address, + allowSelfSend = allowSelfSend, isCurrentAddress = { toValidate -> senderAddresses.any { it.address == toValidate } }, @@ -53,6 +57,7 @@ class ValidateWalletAddressUseCase( userWalletId: UserWalletId, network: Network, address: String, + allowSelfSend: Boolean, isCurrentAddress: (String) -> Boolean, ): AddressValidationResult { val decodedXAddress = BlockchainUtils.decodeRippleXAddress(address, network.rawId) @@ -60,7 +65,7 @@ class ValidateWalletAddressUseCase( val addressToValidate = decodedXAddress?.address ?: address val current = isCurrentAddress(addressToValidate) - val isForbidSelfSend = current && !isSelfSendAvailable + val isForbidSelfSend = current && !isSelfSendAvailable && !allowSelfSend val isValidAddress = walletAddressServiceRepository.validateAddress(userWalletId, network, addressToValidate) return when { diff --git a/domain/transaction/src/test/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCaseTest.kt b/domain/transaction/src/test/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCaseTest.kt new file mode 100644 index 0000000000..5e66d72fe9 --- /dev/null +++ b/domain/transaction/src/test/java/com/tangem/domain/transaction/usecase/ValidateWalletAddressUseCaseTest.kt @@ -0,0 +1,207 @@ +package com.tangem.domain.transaction.usecase + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.ResolveAddressResult +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.CryptoCurrencyAddress +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.WalletAddressServiceRepository +import com.tangem.domain.transaction.error.AddressValidation +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.lib.crypto.models.XrpTaggedAddress +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkObject +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Test + +internal class ValidateWalletAddressUseCaseTest { + + private val repository: WalletAddressServiceRepository = mockk() + private val walletManagersFacade: WalletManagersFacade = mockk() + private val useCase = ValidateWalletAddressUseCase( + walletAddressServiceRepository = repository, + walletManagersFacade = walletManagersFacade, + ) + + private val userWalletId: UserWalletId = mockk() + private val network: Network = mockk() + + @Before + fun setUp() { + mockkObject(BlockchainUtils) + every { BlockchainUtils.decodeRippleXAddress(any(), any()) } returns null + every { network.rawId } returns "ethereum" + } + + @After + fun tearDown() { + unmockkObject(BlockchainUtils) + } + + @Test + fun `GIVEN address not in sender list AND address is valid WHEN invoke THEN returns Valid`() = runTest { + val address = "0xRecipient" + val senderAddresses = listOf(senderAddress("0xSender")) + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, address) } returns true + + val result = useCase(userWalletId, network, address, senderAddresses) + + assertThat(result.getOrNull()).isEqualTo(AddressValidation.Success.Valid) + } + + @Test + fun `GIVEN address in sender list AND self-send not available WHEN invoke THEN returns AddressInWallet`() = runTest { + val address = "0xSender" + val senderAddresses = listOf(senderAddress(address)) + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, address) } returns true + + val result = useCase(userWalletId, network, address, senderAddresses) + + assertThat(result.leftOrNull()).isEqualTo(AddressValidation.Error.AddressInWallet) + } + + @Test + fun `GIVEN address in sender list AND self-send available WHEN invoke THEN returns Valid`() = runTest { + val address = "0xSender" + val senderAddresses = listOf(senderAddress(address)) + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns true + coEvery { repository.validateAddress(userWalletId, network, address) } returns true + + val result = useCase(userWalletId, network, address, senderAddresses) + + assertThat(result.getOrNull()).isEqualTo(AddressValidation.Success.Valid) + } + + @Test + fun `GIVEN address in sender list AND self-send not available AND allowSelfSend is true WHEN invoke THEN returns Valid`() = runTest { + val address = "0xSender" + val senderAddresses = listOf(senderAddress(address)) + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, address) } returns true + + val result = useCase(userWalletId, network, address, senderAddresses, allowSelfSend = true) + + assertThat(result.getOrNull()).isEqualTo(AddressValidation.Success.Valid) + } + + @Test + fun `GIVEN invalid address AND resolved as named address WHEN invoke THEN returns ValidNamedAddress`() = runTest { + val address = "vitalik.eth" + val resolvedAddress = "0xResolved" + val senderAddresses = listOf(senderAddress("0xSender")) + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, address) } returns false + coEvery { repository.resolveAddress(userWalletId, network, address) } returns + ResolveAddressResult.Resolved(resolvedAddress) + + val result = useCase(userWalletId, network, address, senderAddresses) + + assertThat(result.getOrNull()).isEqualTo(AddressValidation.Success.ValidNamedAddress(resolvedAddress)) + } + + @Test + fun `GIVEN invalid address AND not resolved WHEN invoke THEN returns InvalidAddress`() = runTest { + val address = "invalid_address" + val senderAddresses = listOf(senderAddress("0xSender")) + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, address) } returns false + coEvery { repository.resolveAddress(userWalletId, network, address) } returns + ResolveAddressResult.NotSupported + + val result = useCase(userWalletId, network, address, senderAddresses) + + assertThat(result.leftOrNull()).isEqualTo(AddressValidation.Error.InvalidAddress) + } + + @Test + fun `GIVEN valid XRP X-address WHEN invoke THEN returns ValidXAddress`() = runTest { + val xAddress = "X7AcgcsBL4L51nv2theWPZRMcGF37HeMBCFMDcaVEEF8Y3q" + val decodedAddress = "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh" + val senderAddresses = listOf(senderAddress("0xSender")) + + every { BlockchainUtils.decodeRippleXAddress(xAddress, any()) } returns + XrpTaggedAddress(address = decodedAddress, destinationTag = null) + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, decodedAddress) } returns true + + val result = useCase(userWalletId, network, xAddress, senderAddresses) + + assertThat(result.getOrNull()).isEqualTo(AddressValidation.Success.ValidXAddress) + } + + @Test + fun `GIVEN valid XRP X-address that decodes to sender address AND self-send not available WHEN invoke THEN returns AddressInWallet`() = runTest { + val xAddress = "X7AcgcsBL4L51nv2theWPZRMcGF37HeMBCFMDcaVEEF8Y3q" + val decodedAddress = "rSenderAddress" + val senderAddresses = listOf(senderAddress(decodedAddress)) + + every { BlockchainUtils.decodeRippleXAddress(xAddress, any()) } returns + XrpTaggedAddress(address = decodedAddress, destinationTag = null) + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, decodedAddress) } returns true + + val result = useCase(userWalletId, network, xAddress, senderAddresses) + + assertThat(result.leftOrNull()).isEqualTo(AddressValidation.Error.AddressInWallet) + } + + @Test + fun `GIVEN null currencyAddresses AND self-send not available WHEN invoke THEN returns AddressInWallet`() = runTest { + val address = "0xSender" + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, address) } returns true + + val result = useCase(userWalletId, network, address, currencyAddresses = null) + + assertThat(result.leftOrNull()).isEqualTo(AddressValidation.Error.AddressInWallet) + } + + @Test + fun `GIVEN currencyAddresses not containing address AND address is valid WHEN invoke THEN returns Valid`() = runTest { + val address = "0xRecipient" + val currencyAddresses = setOf(networkAddress("0xSender")) + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, address) } returns true + + val result = useCase(userWalletId, network, address, currencyAddresses) + + assertThat(result.getOrNull()).isEqualTo(AddressValidation.Success.Valid) + } + + @Test + fun `GIVEN currencyAddresses containing address AND self-send not available AND allowSelfSend is true WHEN invoke THEN returns Valid`() = runTest { + val address = "0xSender" + val currencyAddresses = setOf(networkAddress(address)) + + coEvery { walletManagersFacade.checkSelfSendAvailability(userWalletId, network) } returns false + coEvery { repository.validateAddress(userWalletId, network, address) } returns true + + val result = useCase(userWalletId, network, address, currencyAddresses, allowSelfSend = true) + + assertThat(result.getOrNull()).isEqualTo(AddressValidation.Success.Valid) + } + + private fun senderAddress(address: String): CryptoCurrencyAddress = + CryptoCurrencyAddress(cryptoCurrency = mockk(relaxed = true), address = address) + + private fun networkAddress(value: String): NetworkAddress.Address = + NetworkAddress.Address(value = value, type = NetworkAddress.Address.Type.Primary) +} \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationComponentParams.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationComponentParams.kt index 267b5c896f..3c5613e62e 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationComponentParams.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/subcomponents/destination/SendDestinationComponentParams.kt @@ -16,6 +16,7 @@ sealed class SendDestinationComponentParams { abstract val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource abstract val userWalletId: UserWalletId abstract val cryptoCurrency: CryptoCurrency + abstract val isAllowSelfSend: Boolean data class DestinationParams( override val state: DestinationUM, @@ -27,6 +28,7 @@ sealed class SendDestinationComponentParams { val isBalanceHidingFlow: StateFlow, val currentRoute: Flow, val callback: SendDestinationComponent.ModelCallback, + override val isAllowSelfSend: Boolean = false, ) : SendDestinationComponentParams() data class DestinationBlockParams( @@ -37,5 +39,6 @@ sealed class SendDestinationComponentParams { override val cryptoCurrency: CryptoCurrency, val blockClickEnableFlow: StateFlow, val predefinedValues: PredefinedValues, + override val isAllowSelfSend: Boolean = false, ) : SendDestinationComponentParams() } \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 6d844d1315..18ff42ff9e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -288,6 +288,7 @@ internal class SendDestinationModel @Inject constructor( network = cryptoCurrency.network, address = address, senderAddresses = senderAddresses.value, + allowSelfSend = params.isAllowSelfSend, ) val memoValidationResult = validateWalletMemoUseCase( userWalletId = userWalletId, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt index 9adf52cf8f..2a845f5472 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt @@ -188,6 +188,7 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( userWalletId = params.userWalletId, cryptoCurrency = secondaryCryptoCurrency, callback = model, + isAllowSelfSend = true, ), ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index 1b837de7c9..d5728b155e 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -80,6 +80,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), cryptoCurrency = model.secondaryCurrency, predefinedValues = PredefinedValues.Empty, + isAllowSelfSend = true, ), onResult = model::onDestinationResult, onClick = model::showEditDestination, From a3b65cd19417c0b9366545f526c9a4c35aab307d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Apr 2026 01:30:04 -0700 Subject: [PATCH 57/75] Updated on 2026-08-14 --- .../DefaultPaymentAccountStatusFetcher.kt | 18 +++++++- .../TangemPayMainScreenCustomerInfoUseCase.kt | 42 ++++--------------- .../TangemPayUpdateInfoStateTransformer.kt | 2 +- 3 files changed, 25 insertions(+), 37 deletions(-) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index 57cc801313..566e56dfbd 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -148,7 +148,23 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( -> PaymentAccountStatusValue.IssuingCard(source = StatusSource.ACTUAL) OrderStatus.CANCELED -> { - PaymentAccountStatusValue.Error.CardIssueFailed(customerId = orderData.customerId) + onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId) + .fold( + ifLeft = { + PaymentAccountStatusValue.Error.CardIssueFailed( + customerId = orderData.customerId, + ) + }, + ifRight = { customerInfo -> + if (customerInfo.kycStatus == KycStatus.REJECTED) { + customerInfo.mapToPaymentAccountStatus() + } else { + PaymentAccountStatusValue.Error.CardIssueFailed( + customerId = orderData.customerId, + ) + } + }, + ) } OrderStatus.COMPLETED -> { // Order was completed -> clear order id and get customer info diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 180b1b864f..73a46626d2 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -3,7 +3,6 @@ package com.tangem.domain.pay.usecase import arrow.core.Either import arrow.core.left import arrow.core.right -import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.model.* @@ -149,41 +148,14 @@ class TangemPayMainScreenCustomerInfoUseCase( error.mapErrorForCustomer().left() }, ifRight = { orderData -> - when (orderData.status) { - OrderStatus.NEW, - OrderStatus.PROCESSING, - -> { - onboardingRepository.getCustomerInfo(userWalletId = userWalletId) - .mapLeft { it.mapErrorForCustomer() } - .map { customerInfo -> - MainScreenCustomerInfo(info = customerInfo, orderStatus = orderData.status) - } - } - - // Order cancelled. No need to get customer info - OrderStatus.CANCELED -> { - MainScreenCustomerInfo( - info = CustomerInfo( - customerId = null, - productInstance = null, - kycStatus = KycStatus.INIT, - cardInfo = null, - ), - orderStatus = OrderStatus.CANCELED, - ).right() - } - - OrderStatus.COMPLETED, - OrderStatus.UNKNOWN, - -> { - onboardingRepository.clearOrderId(userWalletId) - onboardingRepository.getCustomerInfo(userWalletId = userWalletId) - .mapLeft { it.mapErrorForCustomer() } - .map { customerInfo -> - MainScreenCustomerInfo(info = customerInfo, orderStatus = orderData.status) - } - } + if (orderData.status in setOf(OrderStatus.COMPLETED, OrderStatus.UNKNOWN)) { + onboardingRepository.clearOrderId(userWalletId) } + onboardingRepository.getCustomerInfo(userWalletId = userWalletId) + .mapLeft { it.mapErrorForCustomer() } + .map { customerInfo -> + MainScreenCustomerInfo(info = customerInfo, orderStatus = orderData.status) + } }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt index 9c19535642..3e1615a6e2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt @@ -53,9 +53,9 @@ internal class TangemPayUpdateInfoStateTransformer( // when statement copied to WalletTangemPayAnalyticsEventSender. Be careful when editing. return when { - value.orderStatus == OrderStatus.CANCELED -> createCancelledState(customerId) value.info.kycStatus != KycStatus.APPROVED && !value.info.customerId.isNullOrEmpty() -> createKycInProgressState(kycStatus = value.info.kycStatus, customerId = customerId) + value.orderStatus == OrderStatus.CANCELED -> createCancelledState(customerId) cardInfo != null && productInstance != null && value.orderStatus == OrderStatus.COMPLETED -> getCardInfoState(customerId, cardInfo, productInstance) else -> createIssueProgressState() From d22bfd0c87d8fe557bb8efad3d3644f7a6b4c3d8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Apr 2026 15:15:08 +0500 Subject: [PATCH 58/75] Updated on 2026-08-14 --- .../GiveTxPermisssionBottomSheet.kt | 1 + .../permission/state/GiveTxPermissionState.kt | 1 + core/res/src/main/res/values-de/strings.xml | 2 +- core/res/src/main/res/values-es/strings.xml | 2 +- core/res/src/main/res/values-fr/strings.xml | 2 +- core/res/src/main/res/values-it/strings.xml | 2 +- core/res/src/main/res/values-ja/strings.xml | 26 ++- .../src/main/res/values-pt-rBR/strings.xml | 2 +- core/res/src/main/res/values-ru/strings.xml | 2 +- .../src/main/res/values-uk-rUA/strings.xml | 2 +- .../src/main/res/values-zh-rTW/strings.xml | 2 +- core/res/src/main/res/values/strings.xml | 7 +- .../approval/api/GiveApprovalComponent.kt | 1 + .../impl/DefaultGiveApprovalComponent.kt | 20 +- .../approval/impl/model/GiveApprovalModel.kt | 213 ++++++++++++++---- .../approval/impl/model/GiveApprovalUM.kt | 1 + .../approval/impl/ui/GiveApprovalContent.kt | 105 +++++---- .../send/v2/api/params/FeeSelectorParams.kt | 3 + .../DefaultFeeSelectorBlockComponent.kt | 1 + .../v2/feeselector/model/FeeSelectorLogic.kt | 1 + .../model/transformers/FeeItemConverter.kt | 7 +- .../FeeSelectorLoadedTransformer.kt | 2 + .../ShowApprovalBottomSheetTransformer.kt | 1 + .../feature/swap/domain/SwapInteractorImpl.kt | 60 ++--- .../swap/domain/models/ui/SwapState.kt | 1 + .../feature/swap/DefaultSwapComponent.kt | 13 +- .../tangem/feature/swap/ui/StateBuilder.kt | 1 + 27 files changed, 342 insertions(+), 139 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index 7af3019d50..0c56512c7c 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -330,6 +330,7 @@ private val previewData = GiveTxPermissionBottomSheetConfig( approveType = ApproveType.LIMITED, approveButton = ApprovePermissionButton(true) {}, cancelButton = CancelPermissionButton(true), + isResetApproval = false, onChangeApproveType = { ApproveType.LIMITED }, subtitle = resourceReference(R.string.give_permission_staking_subtitle, wrappedList("1")), dialogText = resourceReference(R.string.give_permission_staking_footer), diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt index baa901aeba..5b4c90cc6c 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/state/GiveTxPermissionState.kt @@ -25,6 +25,7 @@ sealed class GiveTxPermissionState { val approveItems: ImmutableList = ApproveType.entries.toImmutableList(), val approveButton: ApprovePermissionButton, val cancelButton: CancelPermissionButton, + val isResetApproval: Boolean, val onChangeApproveType: ((ApproveType) -> Unit)? = null, val onOpenLearnMoreAboutApproveClick: () -> Unit, ) : GiveTxPermissionState() diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 5132421c98..07bf709771 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1620,7 +1620,7 @@ Service vorübergehend nicht verfügbar Daten können derzeit nicht angezeigt werden, Kartenzahlungen funktionieren jedoch weiterhin. Satz \nPIN-Code - Nicht synchronisiert + Sitzung abgelaufen Zugang wiederherstellen Nutzen Sie USDC für alltägliche Zahlungen Tangem Pay ist vorübergehend nicht erreichbar. diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 1b40ed9ebf..b257cbe438 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1593,7 +1593,7 @@ Servicio temporalmente no disponible No es posible mostrar los datos en este momento, pero los pagos con tarjeta siguen funcionando. Establecer \nCódigo PIN - Sincronización necesaria + Sesión expirada Restablecer acceso Usa USDC para pagos cotidianos Tangem Pay temporalmente no disponible diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 772f4d92d2..9075ee073a 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1590,7 +1590,7 @@ Service temporairement indisponible Les données ne peuvent pas être affichées pour le moment, mais les paiements par carte fonctionnent toujours. Définir le \ncode PIN - Synchronisation requise + Session expirée Restaurer l\'accès Utilisez USDC pour les paiements quotidiens Tangem Pay est temporairement indisponible diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 0282ef7661..917756add9 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -177,7 +177,7 @@ Stiamo risolvendo un problema tecnico. Riprova più tardi. Servizio temporaneamente non disponibile Al momento non è possibile visualizzare i dati, ma i pagamenti con carta continuano a funzionare. - Sincronizzazione necessaria + Sessione scaduta Usa USDC per i pagamenti quotidiani Tangem Pay è temporaneamente non disponibile Tangem Pay diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 6dadf3d9d5..404b67969c 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -467,6 +467,19 @@ 他のネットワークで資産を送金すると、永久に失われます。 %sネットワーク 下記のみを使用して資金を送金する + 動的アドレス + 動的アドレスが有効になっています。カスタムの「change」と「index」は利用できません。 + 動的アドレスが有効です。 + 取引ごとに新しいアドレスを使うことで、追跡されにくくなり、オンチェーン上のプライバシーが向上します。 + プライバシー強化 + UTXOベースのネットワークでは、アドレスが自動生成されるため、手動で管理しなくても簡単に資金を受け取れます。 + スムーズな受け取り + 動的アドレスを有効にする + 動的アドレスでは、プライバシー強化のため毎回新しいアドレスが生成されますが、合計残高は変わりません。 + 一部のカスタムアドレスまたはトークンで通常と異なる導出パスが使われているため、動的アドレスを有効にできません。 + 動的アドレスは利用できません + 現在、プロバイダーに接続できません。しばらくしてからもう一度お試しください。 + サービスを利用できません。しばらくしてからもう一度お試しください。 おすすめ 絞り込みを解除 リストは現在更新中のため、一時的に空になっています。しばらくしてからご確認ください。 @@ -745,7 +758,10 @@ データなし マーケット動向 クイックアクション + すべてクリア トークンを探す + 最近 + ポートフォリオ内 結果 時価総額10万ドル以下のトークンを見る トークンを表示 @@ -820,9 +836,11 @@ 時価総額に基づくすべてのコイン間の暗号資産評価における位置 市場格付け 最大供給量 + 流通量と最大供給量 特定の暗号資産に存在しうるコインまたはトークンの最大数 最大供給量 指標 + 制限なし 公式リンク 値動き リポジトリ @@ -1095,6 +1113,8 @@ 認識できないQRコード このネットワークは、追加されているトークンのいずれにも対応していません。対応しているトークンを追加してから暗号資産を送金してください。 対応しているトークンが見つかりません + このQRコードには認識できないパラメータが含まれています:%s。続行すると、一部の支払い情報が失われる可能性があります。 + 不明なパラメータ メモ不要 %3$sネットワーク上の%1$s ( %2$s ) %2$sネットワーク上の%1$s @@ -1477,6 +1497,7 @@ 手数料見積りエラーです。サポートにフィードバックをお送りください。 スワップする 選択したトークンをこの量を交換すると、価格に大きな影響が生じ、結果が減少します。 + 流動性が低いため、受取額が大幅に少なくなる可能性があります。金額を減らすか、別のプロバイダーをお試しください。 価格への影響が甚大です 残高不足 この取引を完了するには残高が不足しています。受け取り額を減らすか、資金を追加してください。 @@ -1486,6 +1507,8 @@ 受け取る トークンを選択 利用不可 + この取引に十分な流動性がありません。\n金額を減らすか、別のプロバイダーを選択してください。 + 取引額が大きすぎます 皆様からのフィードバックをお待ちしております Tangem Payのベータ版を公開しました カードが凍結されています @@ -1609,7 +1632,7 @@ サービスは一時的に利用できません 現在、データを表示できませんが、カードでのお支払いは引き続きご利用いただけます。 \nPINコードの設定 - 未同期 + セッションの有効期限が切れました アクセスを復元 日常の支払いにUSDCを利用 Tangem Payは現在一時的に利用できません。 @@ -1646,6 +1669,7 @@ %%image%% %1$sネットワーク上のトークン %1$s ( %2$s ) トークンは%3$sネットワークの主要通貨であり、このネットワーク上の他のトークンがリストにある限り、非表示にすることはできません。 %sを非表示にできません + 該当なし QRコードを表示 このトークンは、2月%2$s-%3$s の間、%1$s のサービス手数料で別のトークンと交換できます。 Changellyでスワップ、手数料%s diff --git a/core/res/src/main/res/values-pt-rBR/strings.xml b/core/res/src/main/res/values-pt-rBR/strings.xml index c6642f4bfa..94c47b4783 100644 --- a/core/res/src/main/res/values-pt-rBR/strings.xml +++ b/core/res/src/main/res/values-pt-rBR/strings.xml @@ -1631,7 +1631,7 @@ Serviço temporariamente indisponível Não foi possível exibir os detalhes. No entanto, os pagamentos com cartão ainda estão funcionando. Defina o código PIN. - Não sincronizado + Sessão expirada Restaurar acesso Use USDC para pagamentos do dia a dia. O serviço Tangem Pay está temporariamente inacessível. diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 2654c8ae24..67dfaf1848 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1624,7 +1624,7 @@ Мы устраняем техническую проблему. Пожалуйста, попробуйте позже. Сервис временно недоступен Не можем показать данные карты, но оплаты продолжают работать. - Не синхронизирован + Сессия истекла Оплачивайте ежедневные покупки в USDC Tangem Pay временно недоступен Tangem Pay 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 67cdf8af99..8df7a978d6 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1633,7 +1633,7 @@ Сервіс тимчасово недоступний Не можемо показати дані картки, але оплати продовжують працювати. Встановіть \nPIN-код - Не синхронізовано + Сесія закінчилася Відновити доступ Використовуйте USDC для щоденних платежів Tangem Pay тимчасово недоступний diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index c18d1cec74..a6205964b9 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -419,7 +419,7 @@ 我们正在修复技术问题。请稍后再试。 服務暫時無法使用 目前無法顯示資料,但卡片支付仍可正常使用。 - 需要同步 + 工作階段已過期 使用 USDC 進行日常支付 Tangem Pay暂时不可用 Tangem Pay diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 53ed50688f..d3754c9d92 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -422,6 +422,7 @@ Token already exists Decimal must be a valid integer, up to %d Custom derivation + Dynamic Addresses is enabled. Custom \\"change\\" and \\"index\\" are unavailable. E. g. m/00\'/0000\'/0\'/0/0 Enter custom derivation Decimals @@ -475,6 +476,7 @@ %s network Send funds using only Dynamic addresses + Dynamic Addresses is enabled. Custom \"change\" and \"index\" are unavailable. Dynamic addresses enabled Use a new address for each transaction to reduce traceability and improve on-chain privacy. Enhanced Privacy @@ -1652,7 +1654,7 @@ Service temporarily unavailable Unable to display details. However, card payments are still working. Set \nPIN code - Not synced + Session expired Restore access Use USDC for everyday payments Tangem Pay is temporarily unreachable @@ -1732,6 +1734,9 @@ We\'ve encountered an error. Error code: %s. Please contact our support. Use %s or scan a card/ring to have access to your wallet Connection failed: This dApp uses Wallet Connect version 1.0, which is not supported. Please ensure the dApp supports Wallet Connect version 2.0 to connect successfully. + Previous permission will be revoked and a new one will be issued. Network will charge token approval fee for each of theses actions. You will see a zero-amount transaction in the history as a revoke evidence. + Transaction exceeds previously granted permission amount.\nUpdate permission to proceed + Update permission Upgrade to hardware wallet Stay up to date with the latest features and news Real-time alerts for transactions, exchanges, and critical updates. diff --git a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt index 10506d2ba5..cb5db01145 100644 --- a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt +++ b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt @@ -16,6 +16,7 @@ interface GiveApprovalComponent : ComposableBottomSheetComponent { val spenderAddress: String, val subtitle: TextReference, val isHoldToConfirm: Boolean = false, + val isResetApproval: Boolean = false, val callback: Callback, ) diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt index 756d19c2be..21fe5969b9 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.R import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent @@ -23,7 +24,6 @@ import com.tangem.features.send.v2.api.params.FeeSelectorParams import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import com.tangem.common.ui.R as CommonUiR internal class DefaultGiveApprovalComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @@ -38,6 +38,7 @@ internal class DefaultGiveApprovalComponent @AssistedInject constructor( params = FeeSelectorParams.FeeSelectorBlockParams( state = FeeSelectorUM.Loading, onLoadFee = { model.loadFee() }, + onDisableCustomFee = { model.shouldDisableCustomFee() }, onLoadFeeExtended = { selectedFeeToken -> model.loadFeeExtended(selectedFeeToken) }, feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, cryptoCurrencyStatus = params.cryptoCurrencyStatus, @@ -71,22 +72,23 @@ internal class DefaultGiveApprovalComponent @AssistedInject constructor( TangemBottomSheet( config = config, containerColor = TangemTheme.colors.background.secondary, - titleText = resourceReference(CommonUiR.string.give_permission_title), + titleText = resourceReference( + if (uiState.isResetApproval) { + R.string.update_approval_permission_title + } else { + R.string.give_permission_title + }, + ), titleAction = TopAppBarButtonUM.Icon( - iconRes = CommonUiR.drawable.ic_information_24, + iconRes = R.drawable.ic_information_24, onClicked = model::showPermissionInfoDialog, ), ) { GiveApprovalContent( currency = currency, subtitle = params.subtitle, - approveType = uiState.approveType, - approveItems = uiState.approveItems, + uiState = uiState, onChangeApproveType = model::onChangeApproveType, - walletInteractionIcon = uiState.walletInteractionIcon, - isApproveEnabled = uiState.isApproveButtonEnabled, - isApproveLoading = uiState.isApproveLoading, - isHoldToConfirm = uiState.isHoldToConfirm, onApproveClick = model::onApproveClick, onCancelClick = model::onCancelClick, onOpenLearnMoreAboutApproveClick = model::onOpenLearnMoreAboutApproveClick, diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt index 9873b3d65a..585086e50e 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt @@ -5,6 +5,7 @@ import arrow.core.Either import arrow.core.getOrElse import arrow.core.left import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionSender.MultipleTransactionSendMode import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.bottomsheet.permission.state.ApproveType @@ -22,8 +23,10 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.models.AllowanceInfo import com.tangem.domain.transaction.models.TransactionFeeExtended import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase +import com.tangem.domain.transaction.usecase.GetAllowanceInfoUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase @@ -32,6 +35,7 @@ import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.approval.api.GiveApprovalComponent import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback +import com.tangem.features.send.v2.api.entity.FeeItem import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -41,15 +45,17 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.math.BigDecimal +import java.math.RoundingMode import javax.inject.Inject @Stable @ModelScoped -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class GiveApprovalModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, + private val getAllowanceInfoUseCase: GetAllowanceInfoUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val getFeeUseCase: GetFeeUseCase, private val getFeeForGaslessUseCase: GetFeeForGaslessUseCase, @@ -77,11 +83,14 @@ internal class GiveApprovalModel @Inject constructor( isApproveButtonEnabled = false, isApproveLoading = false, isHoldToConfirm = params.isHoldToConfirm, + isResetApproval = params.isResetApproval, ), ) private var feeSelectorUM: FeeSelectorUM = FeeSelectorUM.Loading + private var approvalTxList: Map = emptyMap() + override fun onFeeResult(feeSelectorUM: FeeSelectorUM) { this.feeSelectorUM = feeSelectorUM uiState.update { it.copy(isApproveButtonEnabled = feeSelectorUM.isPrimaryButtonEnabled) } @@ -122,83 +131,127 @@ internal class GiveApprovalModel @Inject constructor( ) } - suspend fun prepareApprovalTransaction(): Either { - val cryptoCurrencyStatus = params.cryptoCurrencyStatus - val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token - ?: return Either.Left(IllegalStateException("Currency is not a token")) - - return createApprovalTransactionUseCase( - cryptoCurrencyStatus = cryptoCurrencyStatus, - userWalletId = params.userWalletId, - amount = getApprovalAmount(), - contractAddress = tokenCurrency.contractAddress, - spenderAddress = params.spenderAddress, + suspend fun loadFee(): Either { + return onApprovalTx( + onApprove = { approve -> + getFeeUseCase( + transactionData = approve, + userWallet = userWallet, + network = params.cryptoCurrencyStatus.currency.network, + ).onRight { fee -> + approvalTxList = mapOf(approve to fee) + } + }, + onResetApprove = { (revokeApproval, approve) -> + getFeeUseCase( + transactionData = revokeApproval, + userWallet = userWallet, + network = params.cryptoCurrencyStatus.currency.network, + ).map { revokeFee -> + estimateFeeForResetApproval( + revokeTransactionFee = revokeFee, + revokeApprovalTransaction = revokeApproval, + approvalTransaction = approve, + ) + } + }, ) } - suspend fun loadFee(): Either { - val approvalTransaction = prepareApprovalTransaction() - .getOrElse { return GetFeeError.DataError(it).left() } - - return getFeeUseCase( - transactionData = approvalTransaction, - userWallet = userWallet, - network = params.cryptoCurrencyStatus.currency.network, - ) + fun shouldDisableCustomFee(): Boolean { + return approvalTxList.size > 1 } suspend fun loadFeeExtended(maybeToken: CryptoCurrencyStatus?): Either { - val approvalTransaction = prepareApprovalTransaction() - .getOrElse { return GetFeeError.DataError(it).left() } + val approve = createApprovalTransactionUseCase( + userWalletId = params.userWalletId, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + amount = getApprovalAmount(), + contractAddress = (params.cryptoCurrencyStatus.currency as CryptoCurrency.Token).contractAddress, + spenderAddress = params.spenderAddress, + ).getOrElse { error -> + TangemLogger.e("Failed to create approveTransaction", error) + return GetFeeError.DataError(error).left() + } return if (maybeToken == null) { getFeeForGaslessUseCase( - transactionData = approvalTransaction, + transactionData = approve, userWallet = userWallet, network = params.cryptoCurrencyStatus.currency.network, ) } else { getFeeForTokenUseCase( - transactionData = approvalTransaction, + transactionData = approve, userWallet = userWallet, token = maybeToken.currency, ) } } + private fun estimateFeeForResetApproval( + revokeTransactionFee: TransactionFee, + revokeApprovalTransaction: TransactionData.Uncompiled, + approvalTransaction: TransactionData.Uncompiled, + ) = when (revokeTransactionFee) { + is TransactionFee.Choosable -> { + val approveFee = revokeTransactionFee.copy( + minimum = revokeTransactionFee.minimum.increaseEthereumGasLimitBy(2.toBigDecimal()), + normal = revokeTransactionFee.normal.increaseEthereumGasLimitBy(2.toBigDecimal()), + priority = revokeTransactionFee.priority.increaseEthereumGasLimitBy(2.toBigDecimal()), + ) + approvalTxList = mapOf( + revokeApprovalTransaction to revokeTransactionFee, + approvalTransaction to approveFee, + ) + revokeTransactionFee.copy( + minimum = approveFee.minimum + revokeTransactionFee.minimum, + normal = approveFee.normal + revokeTransactionFee.normal, + priority = approveFee.priority + revokeTransactionFee.priority, + ) + } + is TransactionFee.Single -> { + val approveFee = revokeTransactionFee.copy(normal = revokeTransactionFee.normal) + approvalTxList = mapOf( + revokeApprovalTransaction to revokeTransactionFee, + approvalTransaction to approveFee, + ) + revokeTransactionFee.copy(normal = approveFee.normal + revokeTransactionFee.normal) + } + } + private suspend fun sendApprovalTransaction(): Boolean { val cryptoCurrencyStatus = params.cryptoCurrencyStatus val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return false val feeContent = feeSelectorUM as? FeeSelectorUM.Content ?: return false - val selectedFee = feeContent.selectedFeeItem.fee val feeExtended = feeContent.feeExtraInfo.transactionFeeExtended val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency - val transactionData = createApprovalTransactionUseCase( - cryptoCurrencyStatus = cryptoCurrencyStatus, - userWalletId = params.userWalletId, - amount = getApprovalAmount(), - fee = selectedFee, - contractAddress = tokenCurrency.contractAddress, - spenderAddress = params.spenderAddress, - ).getOrElse { error -> - TangemLogger.e("Failed to create approval transaction", error) - return false + val transactions = approvalTxList.map { (tx, fee) -> + tx.copy( + fee = when (feeContent.selectedFeeItem) { + is FeeItem.Fast -> (fee as? TransactionFee.Choosable)?.priority ?: fee.normal + is FeeItem.Market -> fee.normal + is FeeItem.Slow -> (fee as? TransactionFee.Choosable)?.minimum ?: fee.normal + else -> feeContent.selectedFeeItem.fee + }, + ) } return if (isFeeInTokenCurrency) { createAndSendGaslessTransactionUseCase( userWallet = userWallet, - transactionData = transactionData, + transactionData = transactions.first(), fee = feeExtended, ) } else { sendTransactionUseCase( - txData = transactionData, + txsData = transactions, userWallet = userWallet, network = tokenCurrency.network, + sendMode = MultipleTransactionSendMode.DEFAULT, ) }.fold( ifLeft = { error -> @@ -241,4 +294,88 @@ internal class GiveApprovalModel @Inject constructor( null } } + + private suspend fun onApprovalTx( + onApprove: suspend (TransactionData.Uncompiled) -> Either, + onResetApprove: + suspend (Pair) -> Either, + ): Either { + val cryptoCurrencyStatus = params.cryptoCurrencyStatus + val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token + ?: return GetFeeError.DataError(IllegalStateException("Currency is not a token")).left() + + val amount = params.amount.toBigDecimalOrNull() + ?: return GetFeeError.DataError(IllegalArgumentException("Invalid amount format")).left() + + val allowance = getAllowanceInfoUseCase( + userWalletId = params.userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + spenderAddress = params.spenderAddress, + requiredAmount = amount, + ).getOrElse { error -> + TangemLogger.e("Failed to get allowance info", error) + return GetFeeError.DataError(error).left() + } + + val approve = createApprovalTransactionUseCase( + userWalletId = params.userWalletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + amount = getApprovalAmount(), + contractAddress = tokenCurrency.contractAddress, + spenderAddress = params.spenderAddress, + ).getOrElse { error -> + TangemLogger.e("Failed to create approveTransaction", error) + return GetFeeError.DataError(error).left() + } + + return if (allowance is AllowanceInfo.ResetNeeded) { + val revokeApproval = createApprovalTransactionUseCase( + userWalletId = params.userWalletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + amount = BigDecimal.ZERO, + contractAddress = tokenCurrency.contractAddress, + spenderAddress = params.spenderAddress, + ).getOrElse { error -> + TangemLogger.e("Failed to create revoke approveTransaction", error) + return GetFeeError.DataError(error).left() + } + onResetApprove(revokeApproval to approve) + } else { + onApprove(approve) + } + } + + private fun Fee.increaseEthereumGasLimitBy(multiplier: BigDecimal): Fee { + if (this !is Fee.Ethereum) return this + val increasedGasPrice = amount.value?.movePointRight(amount.decimals) + ?.divide(gasLimit.toBigDecimal(), RoundingMode.HALF_UP) + val increasedGasLimit = gasLimit + .multiply(multiplier.toBigInteger()) + val increasedAmount = amount.copy( + value = increasedGasPrice?.multiply( + increasedGasLimit.toBigDecimal().movePointLeft(amount.decimals), + ), + ) + return when (this) { + is Fee.Ethereum.EIP1559 -> copy(amount = increasedAmount, gasLimit = increasedGasLimit) + is Fee.Ethereum.Legacy -> copy(amount = increasedAmount, gasLimit = increasedGasLimit) + is Fee.Ethereum.TokenCurrency -> error("handle in [REDACTED_TASK_KEY]") + } + } + + private operator fun Fee.plus(otherFee: Fee): Fee { + if (this !is Fee.Ethereum || otherFee !is Fee.Ethereum) return this + val gasLimit = this.gasLimit + val increasedGasPrice = this.amount.value?.movePointRight(this.amount.decimals) + ?.divide(gasLimit.toBigDecimal(), RoundingMode.HALF_UP) + val increasedGasLimit = gasLimit + otherFee.gasLimit + val increasedAmount = this.amount.copy( + value = increasedGasLimit.toBigDecimal().multiply(increasedGasPrice).movePointLeft(this.amount.decimals), + ) + return when (this) { + is Fee.Ethereum.EIP1559 -> copy(amount = increasedAmount, gasLimit = increasedGasLimit) + is Fee.Ethereum.Legacy -> copy(amount = increasedAmount, gasLimit = increasedGasLimit) + is Fee.Ethereum.TokenCurrency -> this + } + } } \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt index 38f5698135..2b4429c482 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt @@ -12,4 +12,5 @@ internal data class GiveApprovalUM( val isApproveButtonEnabled: Boolean, val isApproveLoading: Boolean, val isHoldToConfirm: Boolean = false, + val isResetApproval: Boolean = false, ) \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt index ce70ed4b1c..6b1fd64f72 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt @@ -27,28 +27,24 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.window.PopupProperties import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.core.ui.R import com.tangem.core.ui.components.* import com.tangem.core.ui.components.containers.FooterContainer import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.approval.impl.model.GiveApprovalUM import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import com.tangem.common.ui.R as CommonUiR @Composable @Suppress("LongParameterList") internal fun GiveApprovalContent( currency: String, + uiState: GiveApprovalUM, subtitle: TextReference, - approveType: ApproveType, - approveItems: ImmutableList, onChangeApproveType: (ApproveType) -> Unit, - walletInteractionIcon: Int?, - isApproveEnabled: Boolean, - isApproveLoading: Boolean, - isHoldToConfirm: Boolean, onApproveClick: () -> Unit, onCancelClick: () -> Unit, onOpenLearnMoreAboutApproveClick: () -> Unit, @@ -73,20 +69,21 @@ internal fun GiveApprovalContent( ApprovalInfo( currency = currency, - approveType = approveType, - approveItems = approveItems, + approveType = uiState.approveType, + approveItems = uiState.approveItems, onChangeApproveType = onChangeApproveType, onOpenLearnMoreAboutApproveClick = onOpenLearnMoreAboutApproveClick, feeSelectorBlockComponent = feeSelectorBlockComponent, + isResetApproval = uiState.isResetApproval, ) SpacerH(height = TangemTheme.dimens.spacing20) - if (isHoldToConfirm) { + if (uiState.isHoldToConfirm) { HoldToConfirmButton( - text = stringResourceSafe(id = CommonUiR.string.common_approve), - enabled = isApproveEnabled, - isLoading = isApproveLoading, + text = stringResourceSafe(id = R.string.common_approve), + enabled = uiState.isApproveButtonEnabled, + isLoading = uiState.isApproveLoading, onConfirm = onApproveClick, modifier = Modifier .fillMaxWidth() @@ -94,21 +91,21 @@ internal fun GiveApprovalContent( ) } else { PrimaryButtonIconEnd( - text = stringResourceSafe(id = CommonUiR.string.common_approve), - iconResId = walletInteractionIcon, - showProgress = isApproveLoading, + text = stringResourceSafe(id = R.string.common_approve), + iconResId = uiState.walletInteractionIcon, + showProgress = uiState.isApproveLoading, modifier = Modifier .fillMaxWidth() .padding(horizontal = TangemTheme.dimens.spacing16), onClick = onApproveClick, - enabled = isApproveEnabled, + enabled = uiState.isApproveButtonEnabled, ) } SpacerH12() SecondaryButton( - text = stringResourceSafe(id = CommonUiR.string.common_cancel), + text = stringResourceSafe(id = R.string.common_cancel), modifier = Modifier .fillMaxWidth() .padding(horizontal = TangemTheme.dimens.spacing16), @@ -124,6 +121,7 @@ internal fun GiveApprovalContent( private fun ApprovalInfo( currency: String, approveType: ApproveType, + isResetApproval: Boolean, approveItems: ImmutableList, onChangeApproveType: (ApproveType) -> Unit, onOpenLearnMoreAboutApproveClick: () -> Unit, @@ -131,7 +129,7 @@ private fun ApprovalInfo( ) { FooterContainer( footer = annotatedReference { - append(stringResourceSafe(CommonUiR.string.swap_approve_description)) + append(stringResourceSafe(R.string.swap_approve_description)) append(" ") withLink( link = LinkAnnotation.Clickable( @@ -140,7 +138,7 @@ private fun ApprovalInfo( ), block = { appendColored( - text = stringResourceSafe(CommonUiR.string.common_learn_more), + text = stringResourceSafe(R.string.common_learn_more), color = TangemTheme.colors.text.accent, ) }, @@ -157,7 +155,13 @@ private fun ApprovalInfo( } SpacerH16() FooterContainer( - footer = resourceReference(CommonUiR.string.give_permission_policy_type_footer), + footer = resourceReference( + if (isResetApproval) { + R.string.update_approval_permission_fee_note + } else { + R.string.give_permission_policy_type_footer + }, + ), modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), ) { feeSelectorBlockComponent.Content( @@ -202,7 +206,7 @@ private fun AmountItem( verticalAlignment = Alignment.CenterVertically, ) { Text( - text = stringResourceSafe(id = CommonUiR.string.give_permission_rows_amount, currency), + text = stringResourceSafe(id = R.string.give_permission_rows_amount, currency), color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle1, maxLines = 1, @@ -215,7 +219,7 @@ private fun AmountItem( maxLines = 1, ) Icon( - painter = rememberVectorPainter(ImageVector.vectorResource(id = CommonUiR.drawable.ic_chevron_24)), + painter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.ic_chevron_24)), contentDescription = null, tint = TangemTheme.colors.icon.informative, modifier = Modifier.padding(start = TangemTheme.dimens.spacing2), @@ -275,10 +279,10 @@ private fun DropdownSelector( Text( text = when (item) { ApproveType.LIMITED -> stringResourceSafe( - id = CommonUiR.string.give_permission_current_transaction, + id = R.string.give_permission_current_transaction, ) ApproveType.UNLIMITED -> stringResourceSafe( - id = CommonUiR.string.give_permission_unlimited, + id = R.string.give_permission_unlimited, ) }, color = TangemTheme.colors.text.primary1, @@ -288,7 +292,7 @@ private fun DropdownSelector( SpacerWMax() Icon( painter = rememberVectorPainter( - image = ImageVector.vectorResource(id = CommonUiR.drawable.ic_check_24), + image = ImageVector.vectorResource(id = R.drawable.ic_check_24), ), tint = color, contentDescription = null, @@ -316,13 +320,8 @@ private fun GiveApprovalContentPreview( GiveApprovalContent( currency = params.currency, subtitle = params.subtitle, - approveType = params.approveType, - approveItems = params.approveItems, + uiState = params.uiState, onChangeApproveType = {}, - walletInteractionIcon = params.walletInteractionIcon, - isApproveEnabled = params.isApproveEnabled, - isApproveLoading = params.isApproveLoading, - isHoldToConfirm = false, onApproveClick = {}, onCancelClick = {}, onOpenLearnMoreAboutApproveClick = {}, @@ -334,11 +333,7 @@ private fun GiveApprovalContentPreview( private data class GiveApprovalPreviewParams( val currency: String, val subtitle: TextReference, - val approveType: ApproveType, - val approveItems: ImmutableList, - val walletInteractionIcon: Int?, - val isApproveEnabled: Boolean, - val isApproveLoading: Boolean, + val uiState: GiveApprovalUM, ) private class GiveApprovalContentPreviewProvider : PreviewParameterProvider { @@ -347,20 +342,36 @@ private class GiveApprovalContentPreviewProvider : PreviewParameterProvider Either)? abstract val onLoadFee: suspend () -> Either + abstract val onDisableCustomFee: () -> Boolean abstract val cryptoCurrencyStatus: CryptoCurrencyStatus abstract val feeCryptoCurrencyStatus: CryptoCurrencyStatus abstract val feeStateConfiguration: FeeStateConfiguration @@ -39,6 +40,7 @@ sealed class FeeSelectorParams { override val analyticsCategoryName: String, override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource, override val shouldShowOnlySpeedOption: Boolean = false, + override val onDisableCustomFee: () -> Boolean = { false }, val bottomSheetShown: (Boolean) -> Unit = {}, ) : FeeSelectorParams() @@ -56,6 +58,7 @@ sealed class FeeSelectorParams { override val analyticsCategoryName: String, override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource, override val shouldShowOnlySpeedOption: Boolean = false, + override val onDisableCustomFee: () -> Boolean = { false }, val callback: FeeSelectorModelCallback, ) : FeeSelectorParams() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt index cd1e7a333d..9cba99909c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt @@ -45,6 +45,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( params = FeeSelectorParams.FeeSelectorDetailsParams( state = model.uiState.value, onLoadFee = params.onLoadFee, + onDisableCustomFee = params.onDisableCustomFee, onLoadFeeExtended = params.onLoadFeeExtended, feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, cryptoCurrencyStatus = params.cryptoCurrencyStatus, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt index ac7220cf8e..5e0a7deecb 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt @@ -110,6 +110,7 @@ internal class FeeSelectorLogic @AssistedInject constructor( feeStateConfiguration = params.feeStateConfiguration, isFeeApproximate = isFeeApproximate(fee.transactionFee.normal.amount.type), feeSelectorIntents = this@FeeSelectorLogic, + shouldDisableCustomFee = params.onDisableCustomFee(), ), ) }, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt index 139d171f9a..a2c55aa0de 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeItemConverter.kt @@ -17,6 +17,7 @@ internal class FeeItemConverter( private val feeSelectorIntents: FeeSelectorIntents, private val appCurrency: AppCurrency, cryptoCurrencyStatus: CryptoCurrencyStatus, + private val shouldDisableCustomFee: Boolean, ) : Converter> { private val customFeeFieldConverter = FeeSelectorCustomFieldConverter( @@ -54,8 +55,10 @@ internal class FeeItemConverter( add(FeeItem.Market(fee = value.transactionFee.normal)) } } - val customFee = value.customFee ?: constructCustomFee() - customFee?.let(::add) + if (!shouldDisableCustomFee) { + val customFee = value.customFee ?: constructCustomFee() + customFee?.let(::add) + } } private fun MutableList.addFeeItemsLimited(value: Input) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt index 21fe42a28e..438b833d03 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/transformers/FeeSelectorLoadedTransformer.kt @@ -22,6 +22,7 @@ internal class FeeSelectorLoadedTransformer( private val feeStateConfiguration: FeeSelectorParams.FeeStateConfiguration, private val isFeeApproximate: Boolean, private val feeSelectorIntents: FeeSelectorIntents, + private val shouldDisableCustomFee: Boolean, ) : Transformer { private val feeItemsConverter = FeeItemConverter( @@ -30,6 +31,7 @@ internal class FeeSelectorLoadedTransformer( feeSelectorIntents = feeSelectorIntents, appCurrency = appCurrency, cryptoCurrencyStatus = feeCryptoCurrencyStatus, + shouldDisableCustomFee = shouldDisableCustomFee, ) override fun transform(prevState: FeeSelectorUM): FeeSelectorUM { 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 0dec39a031..61a3465420 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 @@ -78,6 +78,7 @@ internal class ShowApprovalBottomSheetTransformer( footerText = resourceReference(R.string.staking_give_permission_fee_footer), onChangeApproveType = prevState.clickIntents::onApproveTypeChange, onOpenLearnMoreAboutApproveClick = {}, + isResetApproval = false, ), walletInteractionIcon = walletInterationIcon(userWallet), onCancel = onDismiss, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 7987443ea3..32031b0b71 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -10,7 +10,6 @@ import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionExtras -import com.tangem.blockchain.common.smartcontract.SmartContractCallDataProviderFactory import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplySendCallData @@ -2025,35 +2024,31 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } // setting up amount for approve with given amount for swap [SwapApproveType.Limited] - val callData = SmartContractCallDataProviderFactory.getApprovalCallData( - spenderAddress = requireNotNull(spenderAddress) { "spenderAddress cant be null" }, - amount = swapAmount.value.convertToSdkAmount(fromTokenStatus), - blockchain = fromToken.network.toBlockchain(), - ) - val feeData = try { - val extras = createTransactionExtrasUseCase( - callData = callData, - network = fromToken.network, - ).getOrNull() ?: error("unable to create extras") + val fromAddress = requireNotNull( + fromTokenStatus.value.networkAddress?.defaultAddress?.value, + ) { "networkAddress cant be null" } - val fromAddress = requireNotNull( - fromTokenStatus.value.networkAddress?.defaultAddress?.value, - ) { "networkAddress cant be null" } - val transactionData = TransactionData.Uncompiled( - amount = createNativeAmountForDex("0", fromToken.network), - destinationAddress = fromToken.getContractAddress(), - fee = null, - sourceAddress = fromAddress, - extras = extras, - ) - getFeeUseCase( - transactionData = transactionData, - network = fromToken.network, - userWallet = userWallet, - ).getOrNull() ?: error("unable to calculate fee") - } catch (e: Exception) { - TangemLogger.e("Failed to get fee", e) - // it's impossible next steps without fee + val allowanceInfo = getAllowanceInfoUseCase( + userWalletId = userWalletId, + cryptoCurrency = fromToken, + spenderAddress = requireNotNull(spenderAddress) { "spenderAddress cant be null" }, + requiredAmount = swapAmount.value, + ).getOrNull() + + val amount = if (allowanceInfo is AllowanceInfo.ResetNeeded) { + BigDecimal.ZERO + } else { + swapAmount.value + } + + val approveTransaction = createApprovalTransactionUseCase( + cryptoCurrencyStatus = fromTokenStatus, + userWalletId = userWalletId, + amount = amount, + contractAddress = fromToken.getContractAddress(), + spenderAddress = spenderAddress, + ).getOrElse { error -> + TangemLogger.e("Failed to create approveTransaction", error) return createSwapErrorWith( fromToken = fromTokenStatus, fromAccount = fromAccount, @@ -2063,6 +2058,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } + val feeData = getFeeUseCase( + transactionData = approveTransaction, + network = fromToken.network, + userWallet = userWallet, + ).getOrNull() ?: error("unable to calculate fee") + val feeState = feeData .patchTransactionFeeForSwap(INCREASE_GAS_LIMIT_FOR_DEX) .toTxFeeState(fromToken, null) @@ -2084,6 +2085,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( amount = INFINITY_SYMBOL, walletAddress = getWalletAddress(fromToken.network), spenderAddress = getTokenAddress(fromToken), + isResetApproval = allowanceInfo is AllowanceInfo.ResetNeeded, requestApproveData = RequestApproveStateData( fee = feeState, fromTokenAmount = swapAmount, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index a630100e2e..89f79b2d72 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -87,6 +87,7 @@ sealed class PermissionDataState { val walletAddress: String, val spenderAddress: String, val requestApproveData: RequestApproveStateData, + val isResetApproval: Boolean, ) : PermissionDataState() object PermissionFailed : PermissionDataState() diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index 7d480eb2cf..ed1fb1b425 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -206,10 +206,15 @@ internal class DefaultSwapComponent @AssistedInject constructor( feeCryptoCurrencyStatus = feeCryptoCurrency, amount = model.dataState.amount.orEmpty(), spenderAddress = requireNotNull(model.dataState.approveDataModel).spenderAddress, - subtitle = resourceReference( - id = R.string.give_permission_swap_subtitle, - formatArgs = wrappedList(providerName, permissionState.currency), - ), + subtitle = if (permissionState.isResetApproval) { + resourceReference(R.string.update_approval_permission_subtitle) + } else { + resourceReference( + id = R.string.give_permission_swap_subtitle, + formatArgs = wrappedList(providerName, permissionState.currency), + ) + }, + isResetApproval = permissionState.isResetApproval, isHoldToConfirm = model.isHoldToConfirmEnabled, callback = model.approvalCallback, ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 1f11ab7fa1..ea5e0fe928 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -975,6 +975,7 @@ internal class StateBuilder( dialogText = resourceReference(R.string.swapping_approve_information_text), footerText = resourceReference(R.string.swap_give_permission_fee_footer), onOpenLearnMoreAboutApproveClick = onOpenLearnMoreAboutApproveClick, + isResetApproval = permissionDataState.isResetApproval, ) } } From 6a7391fd738c2ce3eff6e7b139243ec656719c1b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Apr 2026 14:01:51 +0300 Subject: [PATCH 59/75] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 009cf6332a..6b8af7fcd6 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 009cf6332a72cf0893167221abf7010d033906c2 +Subproject commit 6b8af7fcd6579bb723b34769fe95e00a2d058728 From bfeea010d07b8b751eab1b66cd9c50641e6fc05f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Apr 2026 04:30:38 -0700 Subject: [PATCH 60/75] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 9 +++++++++ .../local/preferences/PreferencesKeys.kt | 3 +++ .../datasource/local/visa/TangemPayStorage.kt | 3 +++ .../pay/DefaultTangemPayEligibilityManager.kt | 15 ++++++++++----- .../DefaultPaymentAccountStatusFetcher.kt | 11 +++++++++++ .../repository/DefaultOnboardingRepository.kt | 19 ++++++++++++++++++- .../com/tangem/domain/visa/error/VisaError.kt | 1 + .../tangem/domain/pay/model/CustomerInfo.kt | 18 ++++++++++++++++++ .../pay/model/TangemPayCustomerInfoError.kt | 1 + .../pay/repository/OnboardingRepository.kt | 2 ++ .../TangemPayMainScreenCustomerInfoUseCase.kt | 13 +++++++++++-- .../subscribers/TangemPayMainSubscriber.kt | 5 +++++ 12 files changed, 92 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index 9e6fb969cc..46b35c3bce 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -249,6 +249,15 @@ internal class DefaultTangemPayStorage @Inject constructor( } } + override suspend fun storeIsTangemPayDeactivated(userWalletId: UserWalletId) { + appPreferencesStore.store(PreferencesKeys.getTangemPayDeactivatedKey(userWalletId), true) + } + + override suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean { + val key = PreferencesKeys.getTangemPayDeactivatedKey(userWalletId) + return appPreferencesStore.getSyncOrNull(key) == true + } + override suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) { withContext(dispatcherProvider.io) { secureStorage.delete(createAuthTokensKey(customerWalletAddress)) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 738de04ec7..be52c28b41 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -209,6 +209,9 @@ object PreferencesKeys { fun getTangemPayHideOnboardingKey(userWalletId: UserWalletId) = booleanPreferencesKey("tangem_pay_hide_onboarding_key_$userWalletId") + fun getTangemPayDeactivatedKey(userWalletId: UserWalletId) = + booleanPreferencesKey("tangem_pay_deactivated_$userWalletId") + // endregion } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt index c7540693bc..e6b2a37527 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -56,4 +56,7 @@ interface TangemPayStorage { suspend fun getTangemPayEligibility(): Set suspend fun clearAll(userWalletId: UserWalletId, customerWalletAddress: String) + + suspend fun storeIsTangemPayDeactivated(userWalletId: UserWalletId) + suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt index a54b75b418..fb9bd81fda 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt @@ -84,15 +84,20 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( } private suspend fun getPossibleWalletsForTangemPay(entryPoint: TangemPayEntryPoint?): List { + val wallets = userWalletsListRepository.userWallets.value ?: return emptyList() + + val candidates = wallets.filter { wallet -> + wallet.isMultiCurrency && !wallet.isLocked && wallet.isCompatible() && + !onboardingRepository.isTangemPayDeactivated(wallet.walletId) + } + + if (candidates.isEmpty()) return emptyList() + if (!checkTangemPayEligibility(entryPoint = entryPoint)) { return emptyList() } - val wallets = userWalletsListRepository.userWallets.value ?: return emptyList() - - return wallets.filter { wallet -> - wallet.isMultiCurrency && !wallet.isLocked && wallet.isCompatible() - } + return candidates } private fun UserWallet.isCompatible(): Boolean = when (this) { diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index 566e56dfbd..cdd78706de 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -52,6 +52,16 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( ) } + if (onboardingRepository.isTangemPayDeactivated(params.userWalletId)) { + return@catchOn paymentAccountStatusesStore.store( + userWalletId = params.userWalletId, + status = AccountStatus.Payment( + account = account, + value = PaymentAccountStatusValue.NotCreated, + ), + ) + } + val status = onboardingRepository.hasTangemPayInWallet(userWalletId = params.userWalletId) .fold( ifLeft = { error -> @@ -236,6 +246,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( return when (this) { is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced is VisaApiError.NotPaeraCustomer -> PaymentAccountStatusValue.NotCreated + is VisaApiError.Deactivated -> PaymentAccountStatusValue.NotCreated else -> PaymentAccountStatusValue.Error.Unavailable } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 6880483883..2419f48d7a 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -1,6 +1,8 @@ package com.tangem.data.pay.repository import arrow.core.Either +import arrow.core.flatMap +import arrow.core.left import arrow.core.right import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.api.pay.TangemPayApi @@ -88,7 +90,22 @@ internal class DefaultOnboardingRepository @Inject constructor( override suspend fun getCustomerInfo(userWalletId: UserWalletId): Either { return requestHelper.performRequest(userWalletId) { authHeader -> tangemPayApi.getCustomerMe(authHeader) } - .map { response -> getCustomerInfo(userWalletId = userWalletId, response = response.result) } + .flatMap { response -> + val result = response.result + val status = result?.productInstance?.status + val isDeactivated = status == CustomerMeResponse.ProductInstance.Status.DEACTIVATED + val isBlocked = result?.state?.let { CustomerInfo.State.fromString(it) } == CustomerInfo.State.BLOCKED + if (isDeactivated || isBlocked) { + tangemPayStorage.storeIsTangemPayDeactivated(userWalletId) + VisaApiError.Deactivated.left() + } else { + getCustomerInfo(userWalletId = userWalletId, response = result).right() + } + } + } + + override suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean { + return tangemPayStorage.isTangemPayDeactivated(userWalletId) } override suspend fun clearOrderId(userWalletId: UserWalletId) { diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt index be321d6b33..1ebfcecd9a 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt @@ -57,6 +57,7 @@ sealed class VisaApiError( data object CustomerIsBlocked : VisaApiError(104110210) data object UnknownWithoutCode : VisaApiError(104110999) data class Unknown(override val errorCode: Int) : VisaApiError(errorCode) + data object Deactivated : VisaApiError(0) fun isUnknown() = this is UnknownWithoutCode || this is Unknown diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index 11ef7e59c7..0dbd00d110 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -23,6 +23,24 @@ data class CustomerInfo( val kycStatus: KycStatus, val cardInfo: CardInfo?, ) { + enum class State { + NEW, + ACTIVE, + BLOCKED, + IN_PROGRESS, + UNDEFINED, + ; + + companion object { + fun fromString(value: String) = when (value.uppercase()) { + "NEW" -> NEW + "ACTIVE" -> ACTIVE + "BLOCKED" -> BLOCKED + "IN_PROGRESS" -> IN_PROGRESS + else -> UNDEFINED + } + } + } data class ProductInstance( val id: String, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCustomerInfoError.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCustomerInfoError.kt index e2086cfed3..07e4dc7bb8 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCustomerInfoError.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayCustomerInfoError.kt @@ -6,4 +6,5 @@ sealed interface TangemPayCustomerInfoError { data object RefreshNeededError : TangemPayCustomerInfoError data object UnknownError : TangemPayCustomerInfoError data object ExposedDeviceError : TangemPayCustomerInfoError + data object DeactivatedError : TangemPayCustomerInfoError } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt index d826ae182d..8fcab526ee 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt @@ -35,4 +35,6 @@ interface OnboardingRepository { suspend fun setHideMainOnboardingBanner(userWalletId: UserWalletId) suspend fun disableTangemPay(userWalletId: UserWalletId): Either + + suspend fun isTangemPayDeactivated(userWalletId: UserWalletId): Boolean } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 73a46626d2..04780f63c1 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -29,6 +29,11 @@ class TangemPayMainScreenCustomerInfoUseCase( suspend fun fetch(userWalletId: UserWalletId) { logger.i("fetch: ${userWalletId.stringValue}") + if (onboardingRepository.isTangemPayDeactivated(userWalletId)) { + updateState(userWalletId, MainCustomerInfoContentState.Empty.right()) + return + } + if (deviceSecurity.isSecurityExposed()) { logger.i("fetch security info: rooted: ${deviceSecurity.isRooted}") logger.i("fetch security info: xposed: ${deviceSecurity.isXposed}") @@ -57,8 +62,11 @@ class TangemPayMainScreenCustomerInfoUseCase( } val result = proceedWithPaeraCustomerResult(userWalletId) - .map(MainCustomerInfoContentState::Content) - updateState(userWalletId, result) + if (result.leftOrNull() is TangemPayCustomerInfoError.DeactivatedError) { + updateState(userWalletId, MainCustomerInfoContentState.Empty.right()) + return + } + updateState(userWalletId, result.map(MainCustomerInfoContentState::Content)) } else { // if there's no tangem pay, check eligibility and show onboarding banner showOnboardingBannerIfEligible(userWalletId) @@ -164,6 +172,7 @@ class TangemPayMainScreenCustomerInfoUseCase( return when (this) { is VisaApiError.RefreshTokenExpired -> TangemPayCustomerInfoError.RefreshNeededError is VisaApiError.NotPaeraCustomer -> TangemPayCustomerInfoError.UnknownError + is VisaApiError.Deactivated -> TangemPayCustomerInfoError.DeactivatedError else -> TangemPayCustomerInfoError.UnavailableError } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt index cc818aa976..a800e1a2a7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt @@ -65,6 +65,11 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( TangemPayCustomerInfoError.ExposedDeviceError -> { stateController.update(TangemPayExposedDeviceTransformer(userWalletId)) } + TangemPayCustomerInfoError.DeactivatedError -> { + stateController.update( + transformer = TangemPayHiddenStateTransformer(userWalletId), + ) + } TangemPayCustomerInfoError.UnknownError -> { // hide TangemPay block TangemLogger.e("Failed when loading main screen TangemPay info: $tangemPayError") From 35f073e493a2b406f78b8b8acb4bcb3248d1d565 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Apr 2026 15:30:56 +0400 Subject: [PATCH 61/75] Updated on 2026-08-14 --- .../common/ui/tokens/PortfolioListItem.kt | 234 ++++++++++++ .../ui/components/tokenlist/TokenListItem.kt | 5 +- .../choosetoken/impl/ui/ChooseTokenScreen.kt | 350 ++++++++++++++++++ .../swap/choosetoken/impl/ui/ChooseTokenUM.kt | 23 ++ .../MultiCurrencyAccountContent.kt | 200 +--------- .../multicurrency/MultiCurrencyContent.kt | 30 +- 6 files changed, 616 insertions(+), 226 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/tokens/PortfolioListItem.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenUM.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/PortfolioListItem.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/PortfolioListItem.kt new file mode 100644 index 0000000000..226c6e05d6 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/PortfolioListItem.kt @@ -0,0 +1,234 @@ +package com.tangem.common.ui.tokens + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.animateIntAsState +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.tokenlist.NON_CONTENT_TOKENS_LIST_KEY +import com.tangem.core.ui.components.tokenlist.PortfolioListItem +import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem +import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.conditional +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.utils.lazyListItemPosition + +fun LazyListScope.portfolioTokensList( + portfolio: TokensListItemUM.Portfolio, + portfolioIndex: Int, + testTag: String, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + val tokens = portfolio.tokens + val isExpanded = portfolio.isExpanded + val lastIndex = tokens.lastIndex.inc() + + portfolioItem( + portfolio = portfolio, + modifier = modifier.testModifier(portfolioIndex, testTag), + isBalanceHidden = isBalanceHidden, + ) + val portfolioContent = portfolio.content + if (portfolioContent is PortfolioItemContentUM.Empty) { + item( + key = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}", + contentType = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}", + ) { + SlideInItemVisibility( + currentIndex = 1, + lastIndex = lastIndex, + modifier = modifier + .animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null) + .roundedShapeItemDecoration( + radius = TangemTheme.dimens.radius14, + currentIndex = 1, + lastIndex = 1, + backgroundColor = TangemTheme.colors.background.primary, + ), + visible = isExpanded, + ) { + EmptyAccountContent(portfolioContent) + } + } + return + } + itemsIndexed( + items = tokens, + key = { _, item -> item.id.toString() + "-portfolio-${portfolio.id}" }, + contentType = { _, item -> item::class.java }, + itemContent = { tokenIndex, token -> + val indexWithHeader = tokenIndex.inc() + SlideInItemVisibility( + currentIndex = tokenIndex, + lastIndex = lastIndex, + modifier = modifier + .testModifier(indexWithHeader, testTag) + .animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null) + .roundedShapeItemDecoration( + radius = TangemTheme.dimens.radius14, + currentIndex = indexWithHeader, + lastIndex = lastIndex, + backgroundColor = TangemTheme.colors.background.primary, + ), + visible = isExpanded, + ) { + PortfolioTokensListItem( + state = token, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .conditional(indexWithHeader == lastIndex) { padding(bottom = 8.dp) }, + ) + } + }, + ) +} + +@Suppress("MagicNumber") +fun LazyListScope.portfolioItem(portfolio: TokensListItemUM.Portfolio, modifier: Modifier, isBalanceHidden: Boolean) { + val tokens = portfolio.tokens + val isExpanded = portfolio.isExpanded + val lastIndex = when { + isExpanded && tokens.isEmpty() -> 1 + isExpanded -> tokens.lastIndex.inc() + else -> 0 + } + + item( + key = "account-${portfolio.id}", + contentType = "account-content", + ) { + // Snap immediately on expand; on collapse, hold until all child items finish + // their shrink animation, then snap to fully-rounded shape. + val effectiveLastIndex by animateIntAsState( + targetValue = lastIndex, + animationSpec = if (lastIndex != 0) { + snap() + } else { + snap(delayMillis = minOf(50 * tokens.lastIndex, 250) + 150) + }, + label = "lastIndex", + ) + + PortfolioListItem( + state = portfolio, + isBalanceHidden = isBalanceHidden, + modifier = modifier + .roundedShapeItemDecoration( + currentIndex = 0, + radius = TangemTheme.dimens.radius14, + lastIndex = effectiveLastIndex, + backgroundColor = TangemTheme.colors.background.primary, + ), + ) + } +} + +@Suppress("MagicNumber") +@Composable +fun SlideInItemVisibility( + visible: Boolean, + currentIndex: Int, + lastIndex: Int, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + val maxDelay = 250 + val delayEnter = minOf(50 * currentIndex, maxDelay) + val delayExit = minOf(50 * (lastIndex - currentIndex - 1), maxDelay) + + AnimatedVisibility( + modifier = modifier, + visible = visible, + enter = expandVertically( + tween(200, delayMillis = delayEnter, easing = LinearOutSlowInEasing), + expandFrom = Alignment.Top, + ) + fadeIn(tween(200, delayMillis = delayEnter, easing = LinearOutSlowInEasing)), + exit = shrinkVertically( + tween(150, delayMillis = delayExit, easing = FastOutLinearInEasing), + shrinkTowards = Alignment.Top, + ) + fadeOut(tween(150, delayMillis = delayExit, easing = FastOutLinearInEasing)), + ) { + content() + } +} + +@Composable +private fun EmptyAccountContent(emptyConent: PortfolioItemContentUM.Empty, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SpacerH16() + NonContentItemContent() + val emptyAction = emptyConent.action + if (emptyAction != null) { + SpacerH16() + SecondarySmallButton( + config = SmallButtonConfig( + text = emptyAction.text, + onClick = { emptyAction.onClick() }, + ), + ) + } + SpacerH24() + } +} + +@Composable +fun NonContentItemContent(modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_empty_64), + contentDescription = null, + modifier = Modifier.size(size = TangemTheme.dimens.size64), + tint = TangemTheme.colors.icon.inactive, + ) + + Text( + text = stringResourceSafe(id = R.string.main_empty_tokens_list_message), + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing48), + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + style = TangemTheme.typography.caption2, + ) + } +} + +private fun Modifier.testModifier(index: Int, testTag: String): Modifier = this + .testTag(testTag) + .semantics { lazyListItemPosition = index } \ No newline at end of file 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 index 214c54f967..0c6291b831 100644 --- 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 @@ -2,8 +2,7 @@ package com.tangem.core.ui.components.tokenlist import androidx.compose.animation.* import androidx.compose.animation.SharedTransitionScope.ResizeMode.Companion.scaleToBounds -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween +import androidx.compose.animation.core.* import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -40,6 +39,8 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.ProvideSharedTransitionScope +const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST" + /** * Multi-currency content item * diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt new file mode 100644 index 0000000000..d40b8c87ae --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt @@ -0,0 +1,350 @@ +package com.tangem.feature.swap.choosetoken.impl.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.* +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.semantics +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.common.ui.tokens.portfolioTokensList +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.buttons.common.TangemButtonSize +import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.components.fields.TangemSearchBarDefaults +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.list.InfiniteListHandler +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.token.AccountItemPreviewData +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.TokenListItem +import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM +import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.BuyTokenScreenTestTags +import com.tangem.core.ui.utils.lazyListItemPosition +import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection +import com.tangem.feature.swap.models.TokenListUMData +import com.tangem.feature.swap.models.market.state.SwapMarketState +import com.tangem.feature.swap.presentation.R +import com.tangem.feature.swap.ui.market.swapMarketsListItems +import com.tangem.feature.swap.ui.preview.SwapSelectTokenPreviewProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlin.random.Random + +private const val LOAD_MORE_BUFFER = 25 + +@Composable +internal fun ChooseTokenScreen(state: ChooseTokenUM, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.tertiary) + .fillMaxSize() + .systemBarsPadding() + .imePadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBar(title = state.screenTitle, onBackClick = state.onCloseClick) + SearchBar( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + state = state.searchBar, + colors = TangemSearchBarDefaults.secondaryTextFieldColors, + ) + + Content( + state = state, + modifier = Modifier.weight(1f), + ) + } +} + +@Composable +private fun AppBar(title: TextReference, onBackClick: () -> Unit) { + AppBarWithBackButton( + text = title.resolveReference(), + onBackClick = onBackClick, + iconRes = com.tangem.common.ui.R.drawable.ic_back_24, + modifier = Modifier.height(TangemTheme.dimens.size56), + ) +} + +@Composable +private fun Content(state: ChooseTokenUM, modifier: Modifier = Modifier) { + val nestedScrollConnection = rememberHideKeyboardNestedScrollConnection() + val lazyListState = rememberLazyListState() + + LazyColumn( + modifier = modifier + .fillMaxSize() + .nestedScroll(nestedScrollConnection), + horizontalAlignment = Alignment.CenterHorizontally, + state = lazyListState, + ) { + assetsTitle() + + walletListItem(state.walletList) + + tokensListItems( + tokensListData = state.tokensListData, + isBalanceHidden = state.isBalanceHidden, + ) + + if (state.marketsState != null) { + item("markets_title_spacer") { SpacerH(height = 20.dp) } + swapMarketsListItems(state.marketsState) + } + } + if (state.marketsState != null) { + SetupMarketScrollTracker(state.marketsState, lazyListState) + } +} + +@Composable +private fun SetupMarketScrollTracker(marketsState: SwapMarketState, lazyListState: LazyListState) { + if (marketsState !is SwapMarketState.Content) return + val onLoadMore = remember(marketsState) { + { + marketsState.loadMore() + true + } + } + VisibleItemsTracker( + lazyListState = lazyListState, + marketState = marketsState, + ) + + InfiniteListHandler( + listState = lazyListState, + buffer = LOAD_MORE_BUFFER, + triggerLoadMoreCheckOnItemsCountChange = true, + onLoadMore = onLoadMore, + ) +} + +@Composable +private fun VisibleItemsTracker(lazyListState: LazyListState, marketState: SwapMarketState.Content) { + val visibleItems by remember { + derivedStateOf { + lazyListState.layoutInfo.visibleItemsInfo.mapNotNull { itemInfo -> + marketState.items.find { it.getComposeKey() == itemInfo.key }?.id + } + } + } + + LaunchedEffect(visibleItems) { + marketState.visibleIdsChanged(visibleItems) + } +} + +private fun LazyListScope.assetsTitle() { + item(key = "assets_title") { + Text( + text = stringResourceSafe(R.string.swap_your_assets_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .fillMaxWidth() + .padding( + top = TangemTheme.dimens.spacing20, + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + ), + ) + } +} + +private fun LazyListScope.walletListItem(walletList: WalletListUM) { + if (walletList.items.isEmpty()) return + item("wallet_list") { + Row( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + ) { + walletList.items.forEach { um -> + val colors = when (um.type) { + TangemButtonType.Primary -> TangemButtonsDefaults.primaryButtonColors + else -> TangemButtonsDefaults.secondaryButtonColors + } + TangemButton( + text = um.text?.resolveReference().orEmpty(), + icon = TangemButtonIconPosition.None, + size = TangemButtonSize.Action, + colors = colors, + showProgress = false, + onClick = um.onClick, + enabled = true, + ) + } + } + } +} + +private fun LazyListScope.tokensListItems(tokensListData: TokenListUMData, isBalanceHidden: Boolean) { + when (tokensListData) { + is TokenListUMData.AccountList -> { + tokensListData.tokensList.forEachIndexed { index, item -> + portfolioTokensList( + portfolio = item, + portfolioIndex = index, + isBalanceHidden = isBalanceHidden, + testTag = BuyTokenScreenTestTags.LAZY_LIST_ITEM, + ) + } + } + is TokenListUMData.TokenList -> { + tokensList( + items = tokensListData.tokensList, + isBalanceHidden = isBalanceHidden, + ) + } + TokenListUMData.EmptyList -> Unit + } +} + +private fun LazyListScope.tokensList(items: ImmutableList, isBalanceHidden: Boolean) { + itemsIndexed( + items = items, + key = { _, item -> item.id }, + contentType = { _, item -> item::class.java }, + itemContent = { index, item -> + TokenListItem( + state = item, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = items.lastIndex, + backgroundColor = TangemTheme.colors.background.primary, + ) + .testTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM) + .semantics { lazyListItemPosition = index }, + ) + }, + ) +} + +@Preview +@Composable +private fun TokenScreenPreview(@PreviewParameter(ChooseTokenScreenPreviewProvider::class) state: ChooseTokenUM) { + TangemThemePreview { + ChooseTokenScreen( + state = state, + modifier = Modifier, + ) + } +} + +private val searchBar + get() = SearchBarUM( + placeholderText = resourceReference(R.string.common_search), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = {}, + ) + +private val tokenItem + get() = TokenItemState.Content( + id = Random.nextInt().toString(), + iconState = CurrencyIconState.Locked, + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "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 = {}, + ) + +private val tokens + get() = persistentListOf( + TokensListItemUM.GroupTitle(id = 111, text = stringReference("Network Bitcoin")), + TokensListItemUM.Token(state = tokenItem), + TokensListItemUM.GroupTitle(id = 222, text = stringReference("Network Ethereum")), + TokensListItemUM.Token(state = tokenItem), + TokensListItemUM.Token(state = tokenItem), + TokensListItemUM.Token(state = tokenItem), + ) + +private val accounts + get() = persistentListOf( + TokensListItemUM.Portfolio( + content = PortfolioItemContentUM.Tokens( + tokens = tokens.filterIsInstance().toPersistentList(), + ), + isExpanded = false, + isCollapsable = true, + tokenItemUM = AccountItemPreviewData.accountItem.copy(iconState = AccountItemPreviewData.accountLetterIcon), + ), + TokensListItemUM.Portfolio( + content = PortfolioItemContentUM.Tokens( + tokens = tokens.filterIsInstance().toPersistentList(), + ), + isExpanded = true, + isCollapsable = true, + tokenItemUM = AccountItemPreviewData.accountItem, + ), + ) + +private val wallets + get() = persistentListOf( + TangemButtonUM( + text = TextReference.Str(value = "Wallet 1"), + type = TangemButtonType.Primary, + onClick = {}, + ), + TangemButtonUM( + text = TextReference.Str(value = "Wallet 2"), + type = TangemButtonType.Secondary, + onClick = {}, + ), + TangemButtonUM( + text = TextReference.Str(value = "Wallet 3"), + type = TangemButtonType.Secondary, + onClick = {}, + ), + ) + +private class ChooseTokenScreenPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + ChooseTokenUM( + screenTitle = stringReference("Choose token"), + onCloseClick = {}, + walletList = WalletListUM(wallets), + searchBar = searchBar, + isBalanceHidden = false, + isAfterSearch = false, + tokensListData = TokenListUMData.AccountList( + tokensList = accounts, + accounts.size, + ), + marketsState = SwapSelectTokenPreviewProvider.defaultState.marketsState, + ), + ) +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenUM.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenUM.kt new file mode 100644 index 0000000000..8b9b2e3c23 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenUM.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.swap.choosetoken.impl.ui + +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.feature.swap.models.TokenListUMData +import com.tangem.feature.swap.models.market.state.SwapMarketState +import kotlinx.collections.immutable.ImmutableList + +internal data class ChooseTokenUM( + val screenTitle: TextReference, + val onCloseClick: () -> Unit, + val walletList: WalletListUM, + val searchBar: SearchBarUM, + val isBalanceHidden: Boolean, + val isAfterSearch: Boolean, + val tokensListData: TokenListUMData, + val marketsState: SwapMarketState?, +) + +internal data class WalletListUM( + val items: ImmutableList, +) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt index e9a092a806..e5b93f0c9c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt @@ -1,33 +1,10 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency -import androidx.compose.animation.* -import androidx.compose.animation.core.FastOutLinearInEasing -import androidx.compose.animation.core.LinearOutSlowInEasing -import androidx.compose.animation.core.animateIntAsState -import androidx.compose.animation.core.snap -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.SpacerH24 -import com.tangem.core.ui.components.buttons.SecondarySmallButton -import com.tangem.core.ui.components.buttons.SmallButtonConfig -import com.tangem.core.ui.components.tokenlist.PortfolioListItem -import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem -import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM +import com.tangem.common.ui.tokens.portfolioTokensList 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.core.ui.test.MainScreenTestTags -import com.tangem.core.ui.utils.lazyListItemPosition import kotlinx.collections.immutable.ImmutableList internal fun LazyListScope.portfolioContentItems( @@ -41,178 +18,7 @@ internal fun LazyListScope.portfolioContentItems( modifier = modifier, portfolioIndex = index, isBalanceHidden = isBalanceHidden, + testTag = MainScreenTestTags.TOKEN_LIST_ITEM, ) } -} - -internal fun LazyListScope.portfolioTokensList( - portfolio: TokensListItemUM.Portfolio, - modifier: Modifier, - portfolioIndex: Int, - isBalanceHidden: Boolean, -) { - val tokens = portfolio.tokens - val isExpanded = portfolio.isExpanded - val lastIndex = tokens.lastIndex.inc() - - portfolioItem( - portfolio = portfolio, - modifier = modifier, - portfolioIndex = portfolioIndex, - isBalanceHidden = isBalanceHidden, - ) - val portfolioContent = portfolio.content - if (portfolioContent is PortfolioItemContentUM.Empty) { - item( - key = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}", - contentType = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}", - ) { - SlideInItemVisibility( - currentIndex = 1, - lastIndex = lastIndex, - modifier = modifier - .animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null) - .roundedShapeItemDecoration( - radius = TangemTheme.dimens.radius14, - currentIndex = 1, - lastIndex = 1, - backgroundColor = TangemTheme.colors.background.primary, - ), - visible = isExpanded, - ) { - EmptyAccountContent(portfolioContent) - } - } - return - } - itemsIndexed( - items = tokens, - key = { _, item -> item.id.toString() + "-portfolio-${portfolio.id}" }, - contentType = { _, item -> item::class.java }, - itemContent = { tokenIndex, token -> - val indexWithHeader = tokenIndex.inc() - SlideInItemVisibility( - currentIndex = tokenIndex, - lastIndex = lastIndex, - modifier = modifier - .testModifier(indexWithHeader) - .animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null) - .roundedShapeItemDecoration( - radius = TangemTheme.dimens.radius14, - currentIndex = indexWithHeader, - lastIndex = lastIndex, - backgroundColor = TangemTheme.colors.background.primary, - ), - visible = isExpanded, - ) { - val modifier = if (indexWithHeader == lastIndex) Modifier.padding(bottom = 8.dp) else Modifier - PortfolioTokensListItem( - state = token, - isBalanceHidden = isBalanceHidden, - modifier = modifier, - ) - } - }, - ) -} - -@Composable -private fun EmptyAccountContent(emptyConent: PortfolioItemContentUM.Empty, modifier: Modifier = Modifier) { - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SpacerH16() - NonContentItemContent() - val emptyAction = emptyConent.action - if (emptyAction != null) { - SpacerH16() - SecondarySmallButton( - config = SmallButtonConfig( - text = emptyAction.text, - onClick = { emptyAction.onClick() }, - ), - ) - } - SpacerH24() - } -} - -@Suppress("MagicNumber") -private fun LazyListScope.portfolioItem( - portfolio: TokensListItemUM.Portfolio, - modifier: Modifier, - portfolioIndex: Int, - isBalanceHidden: Boolean, -) { - val tokens = portfolio.tokens - val isExpanded = portfolio.isExpanded - val lastIndex = when { - isExpanded && tokens.isEmpty() -> 1 - isExpanded -> tokens.lastIndex.inc() - else -> 0 - } - - item( - key = "account-${portfolio.id}", - contentType = "account-content", - ) { - // Snap immediately on expand; on collapse, hold until all child items finish - // their shrink animation, then snap to fully-rounded shape. - val effectiveLastIndex by animateIntAsState( - targetValue = lastIndex, - animationSpec = if (lastIndex != 0) { - snap() - } else { - snap(delayMillis = minOf(50 * tokens.lastIndex, 250) + 150) - }, - label = "lastIndex", - ) - - PortfolioListItem( - state = portfolio, - isBalanceHidden = isBalanceHidden, - modifier = modifier - .testModifier(portfolioIndex) - .roundedShapeItemDecoration( - currentIndex = 0, - radius = TangemTheme.dimens.radius14, - lastIndex = effectiveLastIndex, - backgroundColor = TangemTheme.colors.background.primary, - ), - ) - } -} - -@Suppress("MagicNumber") -@Composable -internal fun SlideInItemVisibility( - visible: Boolean, - currentIndex: Int, - lastIndex: Int, - modifier: Modifier = Modifier, - content: @Composable () -> Unit, -) { - val maxDelay = 250 - val delayEnter = minOf(50 * currentIndex, maxDelay) - val delayExit = minOf(50 * (lastIndex - currentIndex - 1), maxDelay) - - AnimatedVisibility( - modifier = modifier, - visible = visible, - enter = expandVertically( - tween(200, delayMillis = delayEnter, easing = LinearOutSlowInEasing), - expandFrom = Alignment.Top, - ) + fadeIn(tween(200, delayMillis = delayEnter, easing = LinearOutSlowInEasing)), - exit = shrinkVertically( - tween(150, delayMillis = delayExit, easing = FastOutLinearInEasing), - shrinkTowards = Alignment.Top, - ) + fadeOut(tween(150, delayMillis = delayExit, easing = FastOutLinearInEasing)), - ) { - content() - } -} - -private fun Modifier.testModifier(index: Int): Modifier = this - .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) - .semantics { lazyListItemPosition = index } \ No newline at end of file +} \ No newline at end of file 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 a04c4bbd1e..1c4838b7b9 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 @@ -7,7 +7,6 @@ import androidx.compose.animation.core.animateIntAsState import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -31,9 +30,12 @@ import androidx.compose.ui.text.lerp import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.common.ui.tokens.NonContentItemContent +import com.tangem.common.ui.tokens.SlideInItemVisibility import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.tokenlist.NON_CONTENT_TOKENS_LIST_KEY import com.tangem.core.ui.components.tokenlist.TokenListItem import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration @@ -59,8 +61,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensLis import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListUM import kotlinx.collections.immutable.ImmutableList -internal const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST" - /** * LazyList extension for [WalletTokensListState] * @@ -522,30 +522,6 @@ private fun LazyListScope.nonContentAccountItem( } } -@Composable -internal fun NonContentItemContent(modifier: Modifier = Modifier) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_empty_64), - contentDescription = null, - modifier = Modifier.size(size = TangemTheme.dimens.size64), - tint = TangemTheme.colors.icon.inactive, - ) - - Text( - text = stringResourceSafe(id = R.string.main_empty_tokens_list_message), - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing48), - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - style = TangemTheme.typography.caption2, - ) - } -} - @Composable internal fun NonContentItemContentV2(textColor: Color, modifier: Modifier = Modifier, onClick: () -> Unit) { Column( From e23c6cf3e29fab9310f0c1bc24dd0b2729023846 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Apr 2026 19:37:31 +0500 Subject: [PATCH 62/75] Updated on 2026-08-14 --- .../tangem/tap/common/clipboard/DefaultClipboardManager.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt b/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt index 478f23edeb..60b622a9af 100644 --- a/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt +++ b/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt @@ -2,6 +2,7 @@ package com.tangem.tap.common.clipboard import android.content.ClipData import android.content.ClipDescription +import android.content.ClipDescription.MIMETYPE_TEXT_HTML import android.content.ClipDescription.MIMETYPE_TEXT_PLAIN import android.os.Build import android.os.PersistableBundle @@ -29,7 +30,9 @@ internal class DefaultClipboardManager(private val clipboardManager: AndroidClip } val clipDescription = clipboardManager.primaryClipDescription - if (clipDescription?.hasMimeType(MIMETYPE_TEXT_PLAIN) == false) { + if (clipDescription?.hasMimeType(MIMETYPE_TEXT_PLAIN) == false && + !clipDescription.hasMimeType(MIMETYPE_TEXT_HTML) + ) { TangemLogger.d("Clipboard doesn't contain text") return default } From ab6832b5e9ed923fb4c0271ff0500b05a3c4481d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Apr 2026 12:52:47 +0300 Subject: [PATCH 63/75] Updated on 2026-08-14 --- .../impl/DefaultTokenMarketBlockComponent.kt | 17 +- .../token/block/impl/ui/TokenMarketBlock.kt | 211 +---------------- .../block/impl/ui/TokenMarketBlockLegacy.kt | 216 +++++++++++++++++ .../DefaultTokenDetailsComponent.kt | 28 ++- .../tokendetails/model/TokenDetailsModel.kt | 34 +++ .../state/TokenDetailsBalanceBlockUM.kt | 64 +++++ .../tokendetails/state/TokenDetailsUM.kt | 24 ++ .../tokendetails/ui/TokenDetailsScreen.kt | 222 ++---------------- .../ui/TokenDetailsScreenLegacy.kt | 218 +++++++++++++++++ ...k.kt => TokenDetailsBalanceBlockLegacy.kt} | 4 +- 10 files changed, 619 insertions(+), 419 deletions(-) create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlockLegacy.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsUM.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreenLegacy.kt rename features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/{TokenDetailsBalanceBlock.kt => TokenDetailsBalanceBlockLegacy.kt} (98%) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/DefaultTokenMarketBlockComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/DefaultTokenMarketBlockComponent.kt index e4acc5b43c..6d719dcfdd 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/DefaultTokenMarketBlockComponent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/DefaultTokenMarketBlockComponent.kt @@ -7,10 +7,12 @@ import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.markets.token.block.TokenMarketBlockComponent.Params import com.tangem.features.markets.token.block.impl.model.TokenMarketBlockModel import com.tangem.features.markets.token.block.impl.ui.TokenMarketBlock +import com.tangem.features.markets.token.block.impl.ui.TokenMarketBlockLegacy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -27,10 +29,17 @@ internal class DefaultTokenMarketBlockComponent @AssistedInject constructor( override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() - TokenMarketBlock( - modifier = modifier, - state = state, - ) + if (LocalRedesignEnabled.current) { + TokenMarketBlock( + tokenMarketBlockUM = state, + modifier = modifier, + ) + } else { + TokenMarketBlockLegacy( + state = state, + modifier = modifier, + ) + } } @AssistedFactory diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt index 362075a2e2..493d0af09c 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt @@ -1,216 +1,25 @@ package com.tangem.features.markets.token.block.impl.ui -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Icon +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.common.ui.charts.MarketChartMini -import com.tangem.common.ui.charts.state.MarketChartRawData -import com.tangem.common.ui.tokens.TokenPriceText -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.SpacerW8 -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.BlockCard -import com.tangem.core.ui.components.marketprice.PriceChangeInPercent -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.token.block.impl.model.formatter.toChartType import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM -import kotlinx.collections.immutable.toImmutableList -import kotlin.random.Random +@Suppress("UnusedParameter") @Composable -internal fun TokenMarketBlock(state: TokenMarketBlockUM, modifier: Modifier = Modifier) { - BlockCard( - modifier = modifier, - enabled = state.currentPrice != null, - onClick = state.onClick, - content = { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(TangemTheme.dimens.spacing12), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - LeftSide( - modifier = Modifier.weight(1f), - symbol = state.currencySymbol, - priceText = state.currentPrice, - percentText = state.h24Percent, - type = state.priceChangeType, - ) - SpacerW8() - RightSide( - modifier = Modifier, - priceChangeType = state.priceChangeType, - chartRawData = state.chartData, - ) - } - }, - ) -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -private fun LeftSide( - symbol: String, - priceText: String?, - percentText: String?, - type: PriceChangeType, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), +internal fun TokenMarketBlock(tokenMarketBlockUM: TokenMarketBlockUM, modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, ) { Text( - text = stringResourceSafe(id = R.string.wallet_marketplace_block_title, symbol), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.subtitle2, + text = "Market Block Redesign", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, ) - - if (priceText != null && percentText != null) { - FlowRow( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - ) { - TokenPriceText( - modifier = Modifier.alignByBaseline(), - price = priceText, - priceChangeType = type, - ) - Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - ) { - PriceChangeInPercent( - modifier = Modifier.alignByBaseline(), - valueInPercent = percentText, - type = type, - ) - Text( - modifier = Modifier.alignByBaseline(), - text = stringResourceSafe(id = R.string.wallet_marketprice_block_update_time), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) - } - } - } else { - TextShimmer( - modifier = Modifier.fillMaxWidth(fraction = 0.6f), - style = TangemTheme.typography.body2, - ) - } - } -} - -@Composable -private fun RightSide( - priceChangeType: PriceChangeType?, - chartRawData: MarketChartRawData?, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier.padding(vertical = TangemTheme.dimens.spacing10), - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - verticalAlignment = Alignment.CenterVertically, - ) { - if (chartRawData != null && priceChangeType != null) { - MarketChartMini( - rawData = chartRawData, - type = priceChangeType.toChartType(), - modifier = Modifier - .requiredSize( - width = TangemTheme.dimens.size56, - height = TangemTheme.dimens.size24, - ), - ) - } else { - RectangleShimmer( - modifier = Modifier - .padding(vertical = TangemTheme.dimens.spacing2) - .requiredSize( - width = TangemTheme.dimens.size56, - height = TangemTheme.dimens.size20, - ), - ) - } - - if (priceChangeType != null) { - Icon( - modifier = Modifier.requiredSize(TangemTheme.dimens.size20), - imageVector = ImageVector.vectorResource(id = R.drawable.ic_chevron_right_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } else { - RectangleShimmer( - modifier = Modifier - .requiredSize( - width = TangemTheme.dimens.size20, - height = TangemTheme.dimens.size20, - ), - ) - } - } -} - -@Preview(widthDp = 360) -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 360) -@Composable -private fun Preview() { - val data = MarketChartRawData( - x = List(20) { Random.nextFloat().toDouble() }.toImmutableList(), - y = List(20) { Random.nextFloat().toDouble() }.toImmutableList(), - ) - - val state = TokenMarketBlockUM( - currencySymbol = "XRP", - currentPrice = "0,5$", - h24Percent = "0,5%", - priceChangeType = PriceChangeType.UP, - chartData = data, - onClick = {}, - ) - - TangemThemePreview { - Column( - modifier = Modifier.background(TangemTheme.colors.background.tertiary), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - ) { - TokenMarketBlock( - modifier = Modifier.fillMaxWidth(), - state = state, - ) - TokenMarketBlock( - modifier = Modifier.fillMaxWidth(), - state = state.copy( - currentPrice = "0,0000000000012356786789$", - ), - ) - TokenMarketBlock( - modifier = Modifier.fillMaxWidth(), - state = state.copy( - currentPrice = null, - chartData = null, - ), - ) - TokenMarketBlock( - modifier = Modifier.fillMaxWidth(), - state = state.copy( - chartData = null, - ), - ) - } } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlockLegacy.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlockLegacy.kt new file mode 100644 index 0000000000..17d3314738 --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlockLegacy.kt @@ -0,0 +1,216 @@ +package com.tangem.features.markets.token.block.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.common.ui.charts.MarketChartMini +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.tokens.TokenPriceText +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerW8 +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.marketprice.PriceChangeInPercent +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.markets.impl.R +import com.tangem.features.markets.token.block.impl.model.formatter.toChartType +import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM +import kotlinx.collections.immutable.toImmutableList +import kotlin.random.Random + +@Composable +internal fun TokenMarketBlockLegacy(state: TokenMarketBlockUM, modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier, + enabled = state.currentPrice != null, + onClick = state.onClick, + content = { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + LeftSide( + modifier = Modifier.weight(1f), + symbol = state.currencySymbol, + priceText = state.currentPrice, + percentText = state.h24Percent, + type = state.priceChangeType, + ) + SpacerW8() + RightSide( + modifier = Modifier, + priceChangeType = state.priceChangeType, + chartRawData = state.chartData, + ) + } + }, + ) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun LeftSide( + symbol: String, + priceText: String?, + percentText: String?, + type: PriceChangeType, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + Text( + text = stringResourceSafe(id = R.string.wallet_marketplace_block_title, symbol), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + ) + + if (priceText != null && percentText != null) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + TokenPriceText( + modifier = Modifier.alignByBaseline(), + price = priceText, + priceChangeType = type, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + PriceChangeInPercent( + modifier = Modifier.alignByBaseline(), + valueInPercent = percentText, + type = type, + ) + Text( + modifier = Modifier.alignByBaseline(), + text = stringResourceSafe(id = R.string.wallet_marketprice_block_update_time), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + } + } + } else { + TextShimmer( + modifier = Modifier.fillMaxWidth(fraction = 0.6f), + style = TangemTheme.typography.body2, + ) + } + } +} + +@Composable +private fun RightSide( + priceChangeType: PriceChangeType?, + chartRawData: MarketChartRawData?, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.padding(vertical = TangemTheme.dimens.spacing10), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + verticalAlignment = Alignment.CenterVertically, + ) { + if (chartRawData != null && priceChangeType != null) { + MarketChartMini( + rawData = chartRawData, + type = priceChangeType.toChartType(), + modifier = Modifier + .requiredSize( + width = TangemTheme.dimens.size56, + height = TangemTheme.dimens.size24, + ), + ) + } else { + RectangleShimmer( + modifier = Modifier + .padding(vertical = TangemTheme.dimens.spacing2) + .requiredSize( + width = TangemTheme.dimens.size56, + height = TangemTheme.dimens.size20, + ), + ) + } + + if (priceChangeType != null) { + Icon( + modifier = Modifier.requiredSize(TangemTheme.dimens.size20), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_chevron_right_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } else { + RectangleShimmer( + modifier = Modifier + .requiredSize( + width = TangemTheme.dimens.size20, + height = TangemTheme.dimens.size20, + ), + ) + } + } +} + +@Preview(widthDp = 360) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 360) +@Composable +private fun Preview() { + val data = MarketChartRawData( + x = List(20) { Random.nextFloat().toDouble() }.toImmutableList(), + y = List(20) { Random.nextFloat().toDouble() }.toImmutableList(), + ) + + val state = TokenMarketBlockUM( + currencySymbol = "XRP", + currentPrice = "0,5$", + h24Percent = "0,5%", + priceChangeType = PriceChangeType.UP, + chartData = data, + onClick = {}, + ) + + TangemThemePreview { + Column( + modifier = Modifier.background(TangemTheme.colors.background.tertiary), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + TokenMarketBlockLegacy( + modifier = Modifier.fillMaxWidth(), + state = state, + ) + TokenMarketBlockLegacy( + modifier = Modifier.fillMaxWidth(), + state = state.copy( + currentPrice = "0,0000000000012356786789$", + ), + ) + TokenMarketBlockLegacy( + modifier = Modifier.fillMaxWidth(), + state = state.copy( + currentPrice = null, + chartData = null, + ), + ) + TokenMarketBlockLegacy( + modifier = Modifier.fillMaxWidth(), + state = state.copy( + chartData = null, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt index 8c3ca7e009..7ac7409c48 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt @@ -15,11 +15,13 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsModel import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDetailsBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreenLegacy import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokenreceive.TokenReceiveComponent @@ -84,15 +86,27 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { - val state by model.uiState.collectAsStateWithLifecycle() val bottomSheet by bottomSheetSlot.subscribeAsState() NavigationBar3ButtonsScrim() - TokenDetailsScreen( - state = state, - tokenMarketBlockComponent = tokenMarketBlockComponent, - txHistoryComponent = txHistoryComponent, - yieldSupplyComponent = yieldSupplyComponent, - ) + + if (LocalRedesignEnabled.current) { + val tokenDetailsUM by model.redesignUiState.collectAsStateWithLifecycle() + + TokenDetailsScreen( + tokenDetailsUM = tokenDetailsUM, + tokenMarketBlockComponent = tokenMarketBlockComponent, + modifier = modifier, + ) + } else { + val state by model.uiState.collectAsStateWithLifecycle() + TokenDetailsScreenLegacy( + state = state, + tokenMarketBlockComponent = tokenMarketBlockComponent, + txHistoryComponent = txHistoryComponent, + yieldSupplyComponent = yieldSupplyComponent, + ) + } + bottomSheet.child?.instance?.BottomSheet() } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index bedcc551ca..1adbb5051c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -27,6 +27,9 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -87,7 +90,11 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.Token import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDetailsBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.TokenDetailsExpressStatusFactory import com.tangem.features.tokendetails.TokenDetailsComponent @@ -99,6 +106,7 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.extensions.isZero import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -197,6 +205,9 @@ internal class TokenDetailsModel @Inject constructor( private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency)) val uiState: StateFlow = internalUiState + private val internalRedesignUiState = MutableStateFlow(createInitialRedesignState()) + val redesignUiState: StateFlow = internalRedesignUiState + // region Clore migration // TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY]) private val cloreMigrationModel by lazy(mode = LazyThreadSafetyMode.NONE) { @@ -1275,6 +1286,29 @@ internal class TokenDetailsModel @Inject constructor( // endregion Clore migration + private fun createInitialRedesignState(): TokenDetailsUM { + return TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + title = stringReference(cryptoCurrency.name), + subtitle = stringReference(cryptoCurrency.symbol), + menuItems = persistentListOf(), + ), + balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( + actionButtons = persistentListOf(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + ), + marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = cryptoCurrency.symbol), + stakingBlocksState = null, + pullToRefreshConfig = PullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ), + isBalanceHidden = false, + isMarketPriceAvailable = false, + ) + } + private companion object { const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt new file mode 100644 index 0000000000..aab3bd7c14 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt @@ -0,0 +1,64 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class TokenDetailsBalanceBlockUM { + + abstract val actionButtons: ImmutableList + abstract val tokenBalanceTypeUM: TokenBalanceTypeUM + abstract val currencyIconState: CurrencyIconState + + data class Loading( + override val actionButtons: ImmutableList, + override val tokenBalanceTypeUM: TokenBalanceTypeUM, + override val currencyIconState: CurrencyIconState, + ) : TokenDetailsBalanceBlockUM() + + data class Content( + override val actionButtons: ImmutableList, + override val tokenBalanceTypeUM: TokenBalanceTypeUM, + override val currencyIconState: CurrencyIconState, + val displayCryptoBalance: TextReference, + val displayFiatBalance: TextReference, + val isBalanceFlickering: Boolean, + ) : TokenDetailsBalanceBlockUM() + + data class Error( + override val actionButtons: ImmutableList, + override val tokenBalanceTypeUM: TokenBalanceTypeUM, + override val currencyIconState: CurrencyIconState, + ) : TokenDetailsBalanceBlockUM() + + fun copyActionButtons(buttons: ImmutableList): TokenDetailsBalanceBlockUM { + return when (this) { + is Content -> this.copy(actionButtons = buttons) + is Error -> this.copy(actionButtons = buttons) + is Loading -> this.copy(actionButtons = buttons) + } + } +} + +internal sealed class TokenBalanceTypeUM { + + abstract val type: Type + + data object Single : TokenBalanceTypeUM() { + override val type = Type.ALL + } + + data class Multiple( + override val type: Type, + val availableTypes: ImmutableList, + val onSelect: (Type) -> Unit, + ) : TokenBalanceTypeUM() + + enum class Type { + ALL, + AVAILABLE, + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsUM.kt new file mode 100644 index 0000000000..14b55185e8 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsUM.kt @@ -0,0 +1,24 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import androidx.compose.runtime.Stable +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +@Stable +internal data class TokenDetailsUM( + val topAppBarUM: TokenDetailsTopAppBarUM, + val balanceBlockUM: TokenDetailsBalanceBlockUM, + val marketPriceBlockState: MarketPriceBlockState, + val stakingBlocksState: StakingBlockUM?, + val pullToRefreshConfig: PullToRefreshConfig, + val isBalanceHidden: Boolean, + val isMarketPriceAvailable: Boolean, +) + +internal data class TokenDetailsTopAppBarUM( + val title: TextReference, + val subtitle: TextReference, + val menuItems: ImmutableList, +) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index a3280e26c4..01902e1cab 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -1,218 +1,30 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.* -import androidx.compose.material3.Scaffold -import androidx.compose.material3.ScaffoldDefaults +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheet -import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig -import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig -import com.tangem.common.ui.expressStatus.expressTransactionsItems -import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer -import com.tangem.core.ui.components.marketprice.MarketPriceBlock -import com.tangem.core.ui.components.marketprice.MarketPriceBlockState -import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.test.TokenDetailsScreenTestTags -import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData -import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheet -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM import com.tangem.features.markets.token.block.TokenMarketBlockComponent -import com.tangem.features.txhistory.component.TxHistoryComponent -import com.tangem.features.txhistory.entity.TxHistoryUM -import com.tangem.features.yield.supply.api.YieldSupplyComponent -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -// TODO: Split to blocks [REDACTED_JIRA] -@Suppress("LongMethod", "CyclomaticComplexMethod") +@Suppress("UnusedParameter") @Composable internal fun TokenDetailsScreen( - state: TokenDetailsState, + tokenDetailsUM: TokenDetailsUM, tokenMarketBlockComponent: TokenMarketBlockComponent?, - txHistoryComponent: TxHistoryComponent, - yieldSupplyComponent: YieldSupplyComponent, + modifier: Modifier = Modifier, ) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - Scaffold( - topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) }, - contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars), - containerColor = TangemTheme.colors.background.secondary, - ) { scaffoldPaddings -> - val listState = rememberLazyListState() - val txHistoryComponentState by txHistoryComponent.txHistoryState.collectAsStateWithLifecycle() - val dialogConfig = state.dialogConfig - val betweenItemsPadding = TangemTheme.dimens.spacing12 - val horizontalPadding = TangemTheme.dimens.spacing16 - val itemModifier = Modifier - .padding(top = betweenItemsPadding) - .padding(horizontal = horizontalPadding) - - TangemPullToRefreshContainer( - config = state.pullToRefreshConfig, - modifier = Modifier.padding(scaffoldPaddings), - ) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .testTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER), - state = listState, - contentPadding = PaddingValues( - bottom = TangemTheme.dimens.spacing16 + bottomBarHeight, - ), - ) { - item { - TokenInfoBlock( - modifier = Modifier.padding(horizontal = horizontalPadding), - state = state.tokenInfoBlockState, - ) - } - item { - TokenDetailsBalanceBlock( - modifier = itemModifier, - isBalanceHidden = state.isBalanceHidden, - state = state.tokenBalanceBlockState, - ) - } - items( - items = state.notifications, - key = { it::class.java }, - contentType = { it.config::class.java }, - itemContent = { - Notification( - modifier = itemModifier.animateItem(), - config = it.config, - iconTint = when (it) { - is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent - is TokenDetailsNotification.UsedOutdatedData -> TangemTheme.colors.text.attention - else -> null - }, - ) - }, - ) - - when { - tokenMarketBlockComponent != null -> { - item( - key = TokenMarketBlockComponent::class.java, - contentType = TokenMarketBlockComponent::class.java, - content = { tokenMarketBlockComponent.Content(modifier = itemModifier) }, - ) - } - state.isMarketPriceAvailable -> { - item( - key = MarketPriceBlockState::class.java, - contentType = MarketPriceBlockState::class.java, - content = { - MarketPriceBlock( - modifier = itemModifier, - state = state.marketPriceBlockState, - ) - }, - ) - } - } - - if (state.stakingBlocksState != null) { - item( - key = StakingBlockUM::class.java, - contentType = StakingBlockUM::class.java, - content = { - TokenStakingBlock( - state = state.stakingBlocksState, - isBalanceHidden = state.isBalanceHidden, - modifier = itemModifier, - ) - }, - ) - } - - item { - yieldSupplyComponent.Content(modifier = itemModifier) - } - - expressTransactionsItems( - expressTxs = state.expressTxsToDisplay, - modifier = itemModifier, - ) - - with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryComponentState) } - } - } - - if (dialogConfig != null) { - TokenDetailsDialogs(dialogConfig = dialogConfig) - } - - state.bottomSheetConfig?.let { config -> - when (config.content) { - is ChooseAddressBottomSheetConfig -> { - ChooseAddressBottomSheet(config = config) - } - is ExpressStatusBottomSheetConfig -> { - ExpressStatusBottomSheet(config = config) - } - is CloreMigrationBottomSheetConfig -> { - CloreMigrationBottomSheet(config = config) - } - } - } - } -} - -// region Preview -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun TokenDetailsScreenPreview( - @PreviewParameter(TokenDetailsScreenParameterProvider::class) state: TokenDetailsState, -) { - TangemThemePreview { - TokenDetailsScreen( - state = state, - tokenMarketBlockComponent = null, - txHistoryComponent = object : TxHistoryComponent { - override val txHistoryState: StateFlow = MutableStateFlow( - value = TxHistoryUM.Empty(isBalanceHidden = false, onExploreClick = {}), - ) - - override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) = Unit - }, - yieldSupplyComponent = object : YieldSupplyComponent { - @Composable - override fun Content(modifier: Modifier) { - } - }, + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + text = "Token Details Redesign", + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, ) } -} - -private class TokenDetailsScreenParameterProvider : CollectionPreviewParameterProvider( - collection = listOf( - TokenDetailsPreviewData.tokenDetailsState_1, - TokenDetailsPreviewData.tokenDetailsState_2, - TokenDetailsPreviewData.tokenDetailsState_3, - ), -) -// endregion Preview \ No newline at end of file +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreenLegacy.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreenLegacy.kt new file mode 100644 index 0000000000..da6b9c913b --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreenLegacy.kt @@ -0,0 +1,218 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui + +import android.content.res.Configuration +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.* +import androidx.compose.material3.Scaffold +import androidx.compose.material3.ScaffoldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheet +import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.expressTransactionsItems +import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer +import com.tangem.core.ui.components.marketprice.MarketPriceBlock +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.TokenDetailsScreenTestTags +import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData +import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlockLegacy +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheet +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock +import com.tangem.features.markets.token.block.TokenMarketBlockComponent +import com.tangem.features.txhistory.component.TxHistoryComponent +import com.tangem.features.txhistory.entity.TxHistoryUM +import com.tangem.features.yield.supply.api.YieldSupplyComponent +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +// TODO: Split to blocks [REDACTED_JIRA] +@Suppress("LongMethod", "CyclomaticComplexMethod") +@Composable +internal fun TokenDetailsScreenLegacy( + state: TokenDetailsState, + tokenMarketBlockComponent: TokenMarketBlockComponent?, + txHistoryComponent: TxHistoryComponent, + yieldSupplyComponent: YieldSupplyComponent, +) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + Scaffold( + topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) }, + contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars), + containerColor = TangemTheme.colors.background.secondary, + ) { scaffoldPaddings -> + val listState = rememberLazyListState() + val txHistoryComponentState by txHistoryComponent.txHistoryState.collectAsStateWithLifecycle() + val dialogConfig = state.dialogConfig + val betweenItemsPadding = TangemTheme.dimens.spacing12 + val horizontalPadding = TangemTheme.dimens.spacing16 + val itemModifier = Modifier + .padding(top = betweenItemsPadding) + .padding(horizontal = horizontalPadding) + + TangemPullToRefreshContainer( + config = state.pullToRefreshConfig, + modifier = Modifier.padding(scaffoldPaddings), + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .testTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER), + state = listState, + contentPadding = PaddingValues( + bottom = TangemTheme.dimens.spacing16 + bottomBarHeight, + ), + ) { + item { + TokenInfoBlock( + modifier = Modifier.padding(horizontal = horizontalPadding), + state = state.tokenInfoBlockState, + ) + } + item { + TokenDetailsBalanceBlockLegacy( + modifier = itemModifier, + isBalanceHidden = state.isBalanceHidden, + state = state.tokenBalanceBlockState, + ) + } + items( + items = state.notifications, + key = { it::class.java }, + contentType = { it.config::class.java }, + itemContent = { notification -> + Notification( + modifier = itemModifier.animateItem(), + config = notification.config, + iconTint = when (notification) { + is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent + is TokenDetailsNotification.UsedOutdatedData -> TangemTheme.colors.text.attention + else -> null + }, + ) + }, + ) + + when { + tokenMarketBlockComponent != null -> { + item( + key = TokenMarketBlockComponent::class.java, + contentType = TokenMarketBlockComponent::class.java, + content = { tokenMarketBlockComponent.Content(modifier = itemModifier) }, + ) + } + state.isMarketPriceAvailable -> { + item( + key = MarketPriceBlockState::class.java, + contentType = MarketPriceBlockState::class.java, + content = { + MarketPriceBlock( + modifier = itemModifier, + state = state.marketPriceBlockState, + ) + }, + ) + } + } + + if (state.stakingBlocksState != null) { + item( + key = StakingBlockUM::class.java, + contentType = StakingBlockUM::class.java, + content = { + TokenStakingBlock( + state = state.stakingBlocksState, + isBalanceHidden = state.isBalanceHidden, + modifier = itemModifier, + ) + }, + ) + } + + item { + yieldSupplyComponent.Content(modifier = itemModifier) + } + + expressTransactionsItems( + expressTxs = state.expressTxsToDisplay, + modifier = itemModifier, + ) + + with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryComponentState) } + } + } + + if (dialogConfig != null) { + TokenDetailsDialogs(dialogConfig = dialogConfig) + } + + state.bottomSheetConfig?.let { config -> + when (config.content) { + is ChooseAddressBottomSheetConfig -> { + ChooseAddressBottomSheet(config = config) + } + is ExpressStatusBottomSheetConfig -> { + ExpressStatusBottomSheet(config = config) + } + is CloreMigrationBottomSheetConfig -> { + CloreMigrationBottomSheet(config = config) + } + } + } + } +} + +// region Preview +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TokenDetailsScreenPreview( + @PreviewParameter(TokenDetailsScreenParameterProvider::class) state: TokenDetailsState, +) { + TangemThemePreview { + TokenDetailsScreenLegacy( + state = state, + tokenMarketBlockComponent = null, + txHistoryComponent = object : TxHistoryComponent { + override val txHistoryState: StateFlow = MutableStateFlow( + value = TxHistoryUM.Empty(isBalanceHidden = false, onExploreClick = {}), + ) + + override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) = Unit + }, + yieldSupplyComponent = object : YieldSupplyComponent { + @Composable + override fun Content(modifier: Modifier) { + } + }, + ) + } +} + +private class TokenDetailsScreenParameterProvider : CollectionPreviewParameterProvider( + collection = listOf( + TokenDetailsPreviewData.tokenDetailsState_1, + TokenDetailsPreviewData.tokenDetailsState_2, + TokenDetailsPreviewData.tokenDetailsState_3, + ), +) +// endregion Preview \ 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/TokenDetailsBalanceBlockLegacy.kt similarity index 98% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt rename to features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlockLegacy.kt index 293e54545c..2e8ba0e4b9 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/TokenDetailsBalanceBlockLegacy.kt @@ -39,7 +39,7 @@ import kotlinx.collections.immutable.toImmutableList @Suppress("DestructuringDeclarationWithTooManyEntries") @Composable -internal fun TokenDetailsBalanceBlock( +internal fun TokenDetailsBalanceBlockLegacy( state: TokenDetailsBalanceBlockState, isBalanceHidden: Boolean, modifier: Modifier = Modifier, @@ -264,7 +264,7 @@ private fun Preview_TokenDetailsBalanceBlock( @PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState, ) { TangemThemePreview { - TokenDetailsBalanceBlock(state = state, isBalanceHidden = false) + TokenDetailsBalanceBlockLegacy(state = state, isBalanceHidden = false) } } From d0c76d4fc4ba45d41d6b26331a131f8ffa5be5db Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 3 Apr 2026 01:58:49 -0700 Subject: [PATCH 64/75] Updated on 2026-08-14 --- .../data/pay/repository/DefaultOnboardingRepository.kt | 4 ++-- .../main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index 2419f48d7a..8253961561 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -94,8 +94,8 @@ internal class DefaultOnboardingRepository @Inject constructor( val result = response.result val status = result?.productInstance?.status val isDeactivated = status == CustomerMeResponse.ProductInstance.Status.DEACTIVATED - val isBlocked = result?.state?.let { CustomerInfo.State.fromString(it) } == CustomerInfo.State.BLOCKED - if (isDeactivated || isBlocked) { + val isFormer = result?.state?.let { CustomerInfo.State.fromString(it) } == CustomerInfo.State.FORMER + if (isDeactivated || isFormer) { tangemPayStorage.storeIsTangemPayDeactivated(userWalletId) VisaApiError.Deactivated.left() } else { diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index 0dbd00d110..a35e9adaea 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.visa.model.TangemPayCardFrozenState import java.math.BigDecimal +import java.util.Locale sealed class MainCustomerInfoContentState { object Loading : MainCustomerInfoContentState() @@ -27,15 +28,17 @@ data class CustomerInfo( NEW, ACTIVE, BLOCKED, + FORMER, IN_PROGRESS, UNDEFINED, ; companion object { - fun fromString(value: String) = when (value.uppercase()) { + fun fromString(value: String) = when (value.uppercase(Locale.US)) { "NEW" -> NEW "ACTIVE" -> ACTIVE "BLOCKED" -> BLOCKED + "FORMER" -> FORMER "IN_PROGRESS" -> IN_PROGRESS else -> UNDEFINED } From ccc55615e3a100e1808358cbe6d19a8d7a348135 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 3 Apr 2026 12:38:36 +0300 Subject: [PATCH 65/75] Updated on 2026-08-14 --- .../kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt index 79dde6cead..ecc4cf0363 100644 --- a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt +++ b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt @@ -25,13 +25,13 @@ internal object ABTestsManagerModule { appScope: AppCoroutineScope, ): ABTestsManager { return if (BuildConfig.AB_TESTS_ENABLED) { - StubABTestsManager() - } else { AmplitudeABTestsManager( application = application, apiKey = environmentConfig.amplitudeApiKey, scope = appScope, ) + } else { + StubABTestsManager() } } } \ No newline at end of file From 119b6cd5f71b53f89078b1d1c897dd19f5840186 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 3 Apr 2026 17:49:33 +0300 Subject: [PATCH 66/75] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 6b8af7fcd6..4bfa9f04b8 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 6b8af7fcd6579bb723b34769fe95e00a2d058728 +Subproject commit 4bfa9f04b8c4e81e31f9a924aeb45a1b024e31ce From de63b38e1577f2d75c5fea53c1421ec96d034b06 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 3 Apr 2026 18:58:35 +0400 Subject: [PATCH 67/75] Updated on 2026-08-14 --- .../tangem/features/approval/impl/model/GiveApprovalModel.kt | 2 ++ .../features/staking/impl/presentation/model/StakingModel.kt | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt index 585086e50e..727c03292f 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt @@ -186,6 +186,8 @@ internal class GiveApprovalModel @Inject constructor( userWallet = userWallet, token = maybeToken.currency, ) + }.onRight { feeExtended -> + approvalTxList = mapOf(approve to feeExtended.transactionFee) } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index f4b678091c..8b4f05de66 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -1483,8 +1483,7 @@ internal class StakingModel @Inject constructor( fun getApprovalParams(): GiveApprovalComponent.Params? { val amountState = value.amountState as? AmountState.Data ?: return null - val validatorState = value.validatorState as? StakingStates.ValidatorState.Data ?: return null - val targetAddress = validatorState.chosenTarget.address + val approval = stakingApproval as? StakingApproval.Needed ?: return null val feeCurrencyStatus = feeCryptoCurrencyStatus ?: return null return GiveApprovalComponent.Params( @@ -1492,7 +1491,7 @@ internal class StakingModel @Inject constructor( cryptoCurrencyStatus = cryptoCurrencyStatus, feeCryptoCurrencyStatus = feeCurrencyStatus, amount = amountState.amountTextField.value, - spenderAddress = targetAddress, + spenderAddress = approval.spenderAddress, subtitle = resourceReference( id = R.string.give_permission_staking_subtitle, formatArgs = wrappedList(cryptoCurrencyStatus.currency.symbol), From 9d978de53d8a1163e679821521691cb145091120 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 3 Apr 2026 19:00:04 +0400 Subject: [PATCH 68/75] Updated on 2026-08-14 --- .claude/skills/fix-crashlytics/SKILL.md | 295 ++++++++++++++++++++++++ .firebaserc | 5 + .gitignore | 3 + .mcp.json | 14 ++ 4 files changed, 317 insertions(+) create mode 100644 .claude/skills/fix-crashlytics/SKILL.md create mode 100644 .firebaserc create mode 100644 .mcp.json diff --git a/.claude/skills/fix-crashlytics/SKILL.md b/.claude/skills/fix-crashlytics/SKILL.md new file mode 100644 index 0000000000..966d765ce3 --- /dev/null +++ b/.claude/skills/fix-crashlytics/SKILL.md @@ -0,0 +1,295 @@ +--- +name: fix-crashlytics +description: Auto-fix Crashlytics crashes from Jira — find [Crashlytics] tasks, analyze crash, fix code, create branches, comment on Jira. Runs on CI without prompts. +allowed-tools: Read, Grep, Glob, Bash, Edit, Write, Agent, mcp__atlassian__getAccessibleAtlassianResources, mcp__atlassian__searchJiraIssuesUsingJql, mcp__atlassian__getJiraIssue, mcp__atlassian__addCommentToJiraIssue, mcp__firebase__crashlytics_get_issue, mcp__firebase__crashlytics_list_events, mcp__firebase__firebase_get_environment +argument-hint: [--dry-run] [--since ] +--- + +Auto-fix Crashlytics crashes reported in Jira. + +**CRITICAL: This skill runs on CI. NEVER ask questions. If anything is ambiguous, make the safer choice or skip the task.** + +## Constants + +- **Jira cloudId**: `tangem.atlassian.net` +- **Firebase project**: `tangemapp` +- **Crashlytics appId (Release)**: `1:721920782444:android:2202a761840271413f2849` +- **Dry-run mode**: check if `$ARGUMENTS` contains `--dry-run`. In dry-run mode, do NOT push branches and do NOT comment on Jira. +- **Since**: check if `$ARGUMENTS` contains `--since `. The value is any valid JQL date expression (e.g., `-3d`, `-1w`, `"2026-03-25"`). Default: `-1d`. +- **Tangem SDK packages** (crashes here cannot be fixed in app code): + - `com.tangem.blockchain` — Blockchain SDK + - `com.tangem.sdk` — Card SDK + - `com.tangem.hot.sdk` — Hot SDK + - `com.tangem.vico` — Vico charting + - `com.tangem.common.card` — Card SDK common + - `com.tangem.common.core` — Card SDK common core + +## Phase 0: Preflight Checks + +Before any work, verify that all required MCP servers and tools are available. **If any check fails, STOP immediately with an error message — do not proceed.** + +### 0a. Verify Atlassian MCP + +Call `mcp__atlassian__getAccessibleAtlassianResources` (no parameters). +- If the call succeeds and returns a list containing `tangem.atlassian.net` — Atlassian MCP is OK. +- If the call fails or the tool is not found — STOP with: `FATAL: Atlassian MCP server is not connected. Run 'claude mcp list' to check server status.` + +### 0b. Verify Firebase MCP + +Call `mcp__firebase__firebase_get_environment` (no parameters). +- If the call succeeds and the response contains `project_id: "tangemapp"` — Firebase MCP is OK and connected to the correct project (configured via `.firebaserc`). +- If the project is different or missing — STOP with: `FATAL: Firebase project mismatch. Expected 'tangemapp'. Check .firebaserc configuration.` +- If the call fails or the tool is not found — STOP with: `FATAL: Firebase MCP server is not connected. Run 'claude mcp list' to check server status.` + +### 0c. Verify Git State + +Run: +```bash +git status --porcelain 2>&1 +``` +- If output is empty (clean working tree) — OK. +- If there are uncommitted changes — STOP with: `FATAL: Working tree is not clean. Commit or stash changes before running this skill.` + +### 0d. Sync with Remote + +```bash +git fetch origin +git checkout develop +git pull origin develop +``` + +Initialize an internal results list to track each ticket's outcome. + +## Phase 1: Find Crashlytics Tasks + +Search Jira for Crashlytics tasks created in the past day: + +- Tool: `mcp__atlassian__searchJiraIssuesUsingJql` +- `cloudId`: `tangem.atlassian.net` +- `jql`: `project = "AND" AND summary ~ "\\[Crashlytics\\]" AND created >= ORDER BY created DESC` + - Use the `--since` argument value, or `-1d` if not provided. +- `maxResults`: `50` +- `fields`: `["summary", "status"]` + +Collect all returned issue keys (e.g., `[REDACTED_TASK_KEY]`). + +If no tasks found, output "No Crashlytics tasks found since " and stop. + +## Phase 2: Filter Out Already-Branched Tasks + +For each ticket key, check if a branch already exists: + +```bash +git branch -a | grep -F "" +``` + +- If a branch is found: record status `Skipped (branch exists)` and remove from the processing list. + +### 2b. Filter by Existing Comment + +For each remaining ticket, check if it was already processed by a previous run: + +- Call `mcp__atlassian__getJiraIssue` with `issueIdOrKey` set to the ticket key and request comments. +- Check if any comment body starts with `**Claude Report**`. +- If such a comment exists: record status `Skipped (already commented)` and remove from the processing list. + +Keep only tickets that passed both filters. + +If no tickets remain after filtering, output the summary table and stop. + +## Phase 3: Process Each Ticket + +Process each remaining ticket sequentially. **Error handling rule**: if ANY step fails for a ticket, record the failure reason, run `git checkout develop && git checkout -- .` to clean up, and continue to the next ticket. + +### Step 3a: Extract Crashlytics Issue ID + +- Call `mcp__atlassian__getJiraIssue` with `responseContentFormat: "markdown"` to get the full description. +- Find the Crashlytics URL in the description. It looks like: + ``` + https://console.firebase.google.com/project/tangemapp/crashlytics/app/android:com.tangem.wallet/issues/ + ``` +- Extract `` from the URL path (the segment after `/issues/` and before `?`). +- If no Crashlytics link found: skip with `Skipped (no Crashlytics link)`. + +### Step 3b: Get Crash Details from Firebase + +- Call `mcp__firebase__crashlytics_get_issue` with: + - `appId`: `1:721920782444:android:2202a761840271413f2849` + - `issueId`: the extracted issue ID +- Call `mcp__firebase__crashlytics_list_events` with: + - `appId`: `1:721920782444:android:2202a761840271413f2849` + - `filter`: `{"issueId": ""}` + - `pageSize`: `1` + +Extract from the response: +- **Exception type and message** (from `subtitle` or `exceptions`) +- **Blame frame**: file name, line number, symbol (method name) +- **Full stacktrace** (from `exceptions` field in events) + +Classify the crash by examining the blame frame and full stacktrace: + +1. **App code**: blame frame is in `com.tangem.wallet` with `owner: DEVELOPER`, OR the first `com.tangem` frame in stacktrace is in app packages (`com.tangem.feature.*`, `com.tangem.core.*`, `com.tangem.data.*`, `com.tangem.domain.*`, `com.tangem.tap.*`, `com.tangem.datasource.*`). → Continue to Step 3c (fix the bug). + +2. **Tangem SDK**: the first `com.tangem` frame in stacktrace belongs to a Tangem SDK package (see Constants). → Go to Step 3b-sdk (comment only, no fix). + +3. **External dependency**: no `com.tangem` frames, or only third-party/Android framework code. → Go to Step 3b-ext (comment only, no fix). + +### Step 3b-ext: Handle External Dependency Crash (comment only) + +When the crash is in an external dependency (third-party library or Android framework), do NOT attempt to fix it. Instead, comment. + +1. Identify the library/framework from the top frames of the stacktrace. + +2. If NOT in `--dry-run` mode, call `mcp__atlassian__addCommentToJiraIssue`: + - `cloudId`: `tangem.atlassian.net` + - `issueIdOrKey`: the ticket key + - `contentFormat`: `markdown` + - `commentBody`: + ``` + **Claude Report** + **Crash location:** + **Exception:** : + **Analysis:** This crash originates in an external dependency (), not in app code. + ``` + +3. Record status as `Commented (external dependency)`. Do NOT create a branch. + +4. Continue to the next ticket. + +### Step 3b-sdk: Handle Tangem SDK Crash (comment only) + +When the crash is in a Tangem SDK package, do NOT attempt to fix it. Instead, analyze and comment. + +1. Identify which SDK is affected from the package name: + - `com.tangem.blockchain` → Blockchain SDK + - `com.tangem.sdk` / `com.tangem.common.card` / `com.tangem.common.core` → Card SDK + - `com.tangem.hot.sdk` → Hot SDK + - `com.tangem.vico` → Vico + +2. Walk the stacktrace to find the first app-code frame (caller context). + +3. Analyze the crash: what exception, what method, what likely input caused it. + +4. If NOT in `--dry-run` mode, call `mcp__atlassian__addCommentToJiraIssue`: + - `cloudId`: `tangem.atlassian.net` + - `issueIdOrKey`: the ticket key + - `contentFormat`: `markdown` + - `commentBody`: + ``` + **Claude Report** + **Crash location:** + **Exception:** : + **App context:** Called from at + **Analysis:** + **Recommendation:** This crash originates in Tangem . A fix requires an SDK update. + ``` + +5. Record status as `Commented (SDK — )`. Do NOT create a branch. + +6. Continue to the next ticket. + +### Step 3c: Find and Read the Crashing File + +1. Extract the simple class name from the blame frame's `symbol` (e.g., `com.tangem.feature.foo.BarClass.method` -> `BarClass`). +2. Use `Glob("**/.kt")` to find the file. +3. If multiple files match, use the full package path from the stacktrace to disambiguate. +4. `Read` the file. Focus on the method and line number from the blame frame. +5. Use `Grep` to understand related types, method signatures, or null-safety context if needed. + +If the file cannot be found: skip with `Skipped (file not found)`. + +### Step 3d: Fix the Bug + +Apply a **minimal, defensive fix** based on the crash type. Do NOT refactor, add features, or clean up surrounding code. + +**Fix patterns by exception type:** + +| Exception | Fix Strategy | +|-----------|-------------| +| `NullPointerException` | Add null-checks. Use `?.` safe calls, `?: return`/`?: default` for fallback. For Moshi-deserialized models where Kotlin non-null types can be JVM-null, cast to nullable: `val x = obj.field as Type?` then null-check. Use `getOrNull()` instead of `[]` for collections. | +| `IndexOutOfBoundsException` | Add bounds checking. Use `getOrNull()`, `firstOrNull()`, `lastOrNull()`. Check `isEmpty()` before indexing. | +| `IllegalStateException` | Check state before access. For `lateinit` crashes: add `::property.isInitialized` check or make property nullable. For Decompose/lifecycle: guard with lifecycle state check. | +| `IllegalArgumentException` | Validate inputs. Use `coerceIn()`, `coerceAtLeast(0)`, `maxOf(0, value)`. For `BigDecimal` formatting issues: handle negative or zero values. | +| `ClassCastException` | Use `as?` safe cast with fallback. | +| `ConcurrentModificationException` | Copy collection before iteration: `.toList()`. | + +**Rules:** +- Only change the file identified in the blame frame. +- Make the smallest possible change that prevents the crash. +- Use `Edit` tool for precise changes (not `Write` for the whole file). +- Follow existing code patterns in the file (logging, error handling style). +- Do NOT add comments explaining the fix — the commit message and Jira comment handle that. + +### Step 3e: Build Verification + +1. Determine the Gradle module from the file path: + - Take the path relative to the project root, up to (not including) `src/`. + - Replace `/` with `:` and prepend `:`. + - Example: `features/tokendetails/impl/src/...` -> `:features:tokendetails:impl` + - Special case: `app/src/...` -> `:app` (use `assembleGoogleDebug` instead of `assembleDebug`) + +2. Run the build: + ```bash + ./gradlew ::assembleDebug + # or for :app module: + ./gradlew :app:assembleGoogleDebug + ``` + +3. If build fails: + - Read the error, attempt to fix it (one retry only). + - If still fails: `git checkout -- .` and skip with `Failed (build failed)`. + +### Step 3f: Create Branch, Commit, Push + +```bash +git checkout develop +git checkout -b bugfix/ +git add +git commit -m " Fix in " +``` + +If NOT in `--dry-run` mode: +```bash +git push -u origin bugfix/ +``` + +Return to develop for the next ticket: +```bash +git checkout develop +``` + +### Step 3g: Comment on Jira + +If NOT in `--dry-run` mode, call `mcp__atlassian__addCommentToJiraIssue`: +- `cloudId`: `tangem.atlassian.net` +- `issueIdOrKey`: the ticket key +- `contentFormat`: `markdown` +- `commentBody`: + ``` + **Claude Report** + **Root cause:** + **Fix:** + **Branch:** bugfix/ + **Affected file:** + ``` + +Record status as `Fixed`. + +## Phase 4: Output Summary + +Output the results as a Markdown table: + +```markdown +## Crashlytics Auto-Fix Summary + +| Ticket | Crash | File | Status | Branch | +|--------|-------|------|--------|--------| +| AND-XXXXX | NPE in ClassName.method | ClassName.kt | Fixed | bugfix/AND-XXXXX | +| AND-YYYYY | IOOB in OtherClass.method | OtherClass.kt | Skipped (branch exists) | — | +| AND-ZZZZZ | ISE in ThirdClass.method | ThirdClass.kt | Failed (build failed) | — | +``` + +After the table, output totals: +``` +**Total:** X tasks found, Y fixed, Z commented (SDK), W skipped, V failed +``` \ No newline at end of file diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000000..cd2d394938 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "tangemapp" + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index c5484c9083..aed7684b11 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ app/src/external/google-services.json # Kotlin Plugin .kotlin/ find-latest-release-branch.output + +# Claude +/.claude/worktrees/ diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000000..009df155b7 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "firebase": { + "type": "stdio", + "command": "npx", + "args": ["-y", "firebase-tools@latest", "mcp"] + }, + "atlassian": { + "type": "stdio", + "command": "npx", + "args": ["-y", "mcp-remote", "https://mcp.atlassian.com/v1/sse"] + } + } +} \ No newline at end of file From 843ec3d04eddff92c261e9f67c9d5ba61adb49db Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 3 Apr 2026 20:00:57 +0400 Subject: [PATCH 69/75] Updated on 2026-08-14 --- .../approval/api/GiveApprovalComponent.kt | 3 +- .../impl/DefaultGiveApprovalComponent.kt | 10 ++-- .../approval/impl/ui/GiveApprovalContent.kt | 47 ++++++++----------- .../impl/presentation/model/StakingModel.kt | 3 +- .../feature/swap/DefaultSwapComponent.kt | 3 +- 5 files changed, 31 insertions(+), 35 deletions(-) diff --git a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt index cb5db01145..ad7009986e 100644 --- a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt +++ b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt @@ -14,7 +14,8 @@ interface GiveApprovalComponent : ComposableBottomSheetComponent { val feeCryptoCurrencyStatus: CryptoCurrencyStatus, val amount: String, val spenderAddress: String, - val subtitle: TextReference, + val amountFooter: TextReference, + val feeFooter: TextReference, val isHoldToConfirm: Boolean = false, val isResetApproval: Boolean = false, val callback: Callback, diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt index 21fe5969b9..5fc9f69311 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt @@ -71,7 +71,7 @@ internal class DefaultGiveApprovalComponent @AssistedInject constructor( TangemBottomSheet( config = config, - containerColor = TangemTheme.colors.background.secondary, + containerColor = TangemTheme.colors.background.tertiary, titleText = resourceReference( if (uiState.isResetApproval) { R.string.update_approval_permission_title @@ -80,17 +80,17 @@ internal class DefaultGiveApprovalComponent @AssistedInject constructor( }, ), titleAction = TopAppBarButtonUM.Icon( - iconRes = R.drawable.ic_information_24, - onClicked = model::showPermissionInfoDialog, + iconRes = R.drawable.ic_close_new_20, + onClicked = model::onCancelClick, ), ) { GiveApprovalContent( currency = currency, - subtitle = params.subtitle, + amountFooter = params.amountFooter, + feeFooter = params.feeFooter, uiState = uiState, onChangeApproveType = model::onChangeApproveType, onApproveClick = model::onApproveClick, - onCancelClick = model::onCancelClick, onOpenLearnMoreAboutApproveClick = model::onOpenLearnMoreAboutApproveClick, feeSelectorBlockComponent = feeSelectorBlockComponent, ) diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt index 6b1fd64f72..694623b67a 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt @@ -43,22 +43,21 @@ import kotlinx.collections.immutable.persistentListOf internal fun GiveApprovalContent( currency: String, uiState: GiveApprovalUM, - subtitle: TextReference, + amountFooter: TextReference, + feeFooter: TextReference, onChangeApproveType: (ApproveType) -> Unit, onApproveClick: () -> Unit, - onCancelClick: () -> Unit, onOpenLearnMoreAboutApproveClick: () -> Unit, feeSelectorBlockComponent: FeeSelectorBlockComponent, modifier: Modifier = Modifier, ) { Column( modifier = modifier - .background(color = TangemTheme.colors.background.secondary) .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, ) { Text( - text = subtitle.resolveReference(), + text = amountFooter.resolveReference(), color = TangemTheme.colors.text.secondary, style = TangemTheme.typography.body2, textAlign = TextAlign.Center, @@ -74,6 +73,7 @@ internal fun GiveApprovalContent( onChangeApproveType = onChangeApproveType, onOpenLearnMoreAboutApproveClick = onOpenLearnMoreAboutApproveClick, feeSelectorBlockComponent = feeSelectorBlockComponent, + feeFooter = feeFooter, isResetApproval = uiState.isResetApproval, ) @@ -102,16 +102,6 @@ internal fun GiveApprovalContent( ) } - SpacerH12() - - SecondaryButton( - text = stringResourceSafe(id = R.string.common_cancel), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing16), - onClick = onCancelClick, - ) - SpacerH16() } } @@ -126,6 +116,7 @@ private fun ApprovalInfo( onChangeApproveType: (ApproveType) -> Unit, onOpenLearnMoreAboutApproveClick: () -> Unit, feeSelectorBlockComponent: FeeSelectorBlockComponent, + feeFooter: TextReference, ) { FooterContainer( footer = annotatedReference { @@ -155,13 +146,11 @@ private fun ApprovalInfo( } SpacerH16() FooterContainer( - footer = resourceReference( - if (isResetApproval) { - R.string.update_approval_permission_fee_note - } else { - R.string.give_permission_policy_type_footer - }, - ), + footer = if (isResetApproval) { + resourceReference(R.string.update_approval_permission_fee_note) + } else { + feeFooter + }, modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), ) { feeSelectorBlockComponent.Content( @@ -319,11 +308,11 @@ private fun GiveApprovalContentPreview( TangemThemePreview { GiveApprovalContent( currency = params.currency, - subtitle = params.subtitle, + amountFooter = params.amountFooter, + feeFooter = params.feeFooter, uiState = params.uiState, onChangeApproveType = {}, onApproveClick = {}, - onCancelClick = {}, onOpenLearnMoreAboutApproveClick = {}, feeSelectorBlockComponent = PreviewFeeSelectorBlockComponent(), ) @@ -332,7 +321,8 @@ private fun GiveApprovalContentPreview( private data class GiveApprovalPreviewParams( val currency: String, - val subtitle: TextReference, + val amountFooter: TextReference, + val feeFooter: TextReference, val uiState: GiveApprovalUM, ) @@ -341,7 +331,8 @@ private class GiveApprovalContentPreviewProvider : PreviewParameterProvider Date: Mon, 6 Apr 2026 09:49:59 +0200 Subject: [PATCH 70/75] Updated on 2026-08-14 --- .../com/tangem/core/ui/components/Fade.kt | 22 +- ...sableModularBottomSheetContentComponent.kt | 5 +- .../domain/search/model/SearchResult.kt | 3 - .../search/usecase/GetSearchResultsUseCase.kt | 37 +-- .../components/earn/DefaultEarnComponent.kt | 22 +- .../components/feed/DefaultFeedComponent.kt | 12 +- .../DefaultMarketsTokenDetailsComponent.kt | 19 +- .../list/DefaultMarketsTokenListComponent.kt | 22 +- .../details/DefaultNewsDetailsComponent.kt | 20 +- .../news/list/DefaultNewsListComponent.kt | 19 +- .../search/DefaultSearchComponent.kt | 19 +- .../features/feed/model/search/SearchModel.kt | 16 +- .../tangem/features/feed/ui/EntryContent.kt | 133 +++++++++-- .../features/feed/ui/earn/EarnContent.kt | 18 +- .../tangem/features/feed/ui/feed/FeedList.kt | 35 ++- .../detailed/MarketsTokenDetailsContent.kt | 6 +- .../feed/ui/market/list/MarketsList.kt | 13 +- .../ui/news/details/NewsDetailsContent.kt | 12 +- .../news/details/components/ArticleDetail.kt | 217 +++++++++--------- .../components/NewsDetailsPlaceholder.kt | 17 +- .../feed/ui/news/list/NewsListContent.kt | 6 +- .../features/feed/ui/search/SearchContent.kt | 15 +- .../ui/search/preview/SearchContentPreview.kt | 2 + .../utils/EntryContentAnimationTransitions.kt | 44 ++-- 24 files changed, 499 insertions(+), 235 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt index f6e54c8b3b..fcc03b10e6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt @@ -12,9 +12,11 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.res.LocalHazeState import com.tangem.core.ui.res.LocalPowerSavingState import com.tangem.core.ui.res.TangemTheme import dev.chrisbanes.haze.HazeProgressive +import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeTint @@ -62,7 +64,11 @@ fun BottomFade(gradientBrush: Brush, modifier: Modifier = Modifier) { * but with a vertical blur. */ @Composable -fun BottomFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) { +fun BottomFadeWithBlur( + backgroundColor: Color, + modifier: Modifier = Modifier, + hazeState: HazeState = LocalHazeState.current, +) { val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } val isPowerSavingState by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState() @@ -76,19 +82,9 @@ fun BottomFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) { .fillMaxWidth() .height(TangemTheme.dimens.size100 + bottomBarHeight) .hazeEffectTangem( - style = HazeStyle( - blurRadius = 20.dp, - tint = HazeTint( - brush = Brush.verticalGradient( - colors = listOf( - Color.Transparent, - backgroundColor, - ), - ), - ), - backgroundColor = Color.Transparent, - ), + state = hazeState, ) { + blurRadius = 20.dp progressive = HazeProgressive.verticalGradient( startIntensity = 0f, endIntensity = 1f, diff --git a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableModularBottomSheetContentComponent.kt b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableModularBottomSheetContentComponent.kt index 64495d533c..0200c621bc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableModularBottomSheetContentComponent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableModularBottomSheetContentComponent.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.decompose +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.State @@ -27,7 +28,9 @@ interface ComposableModularBottomSheetContentComponent { * Renders the main content of the bottom sheet. * @param bottomSheetState The current state of the bottom sheet. Useful for tracking visibility * (e.g., for analytics or lifecycle effects when the sheet is [BottomSheetState.EXPANDED]). + * @param contentPadding Padding to apply as inner scroll offset in scrollable containers, + * allowing content to scroll under an overlaying top bar. Defaults to no padding. */ @Composable - fun Content(bottomSheetState: State, modifier: Modifier) + fun Content(bottomSheetState: State, contentPadding: PaddingValues, modifier: Modifier) } \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt b/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt index 784f334bdb..98a25e4996 100644 --- a/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt +++ b/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt @@ -1,10 +1,7 @@ package com.tangem.domain.search.model -import com.tangem.domain.markets.TokenMarket - data class SearchResult( val textHints: List, val recentTokens: List, val userAssets: List, - val marketTokens: List, ) \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt b/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt index 87e48a0215..b6167208a2 100644 --- a/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt +++ b/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt @@ -3,7 +3,6 @@ package com.tangem.domain.search.usecase import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.markets.TokenMarket import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -13,15 +12,14 @@ import com.tangem.domain.search.model.UserAssetSearchEntry import com.tangem.domain.search.repository.SearchRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map /** * Primary search use case that produces [SearchResult] based on the current query. * * Behavior depends on the query: * - **Empty query** — returns search history: text hints and recently viewed tokens. - * - **Non-empty query** — performs the search across all unlocked user wallets, - * matching currencies by name or symbol, and combines the results with externally provided market tokens. + * - **Non-empty query** — performs the search across all unlocked user wallets, matching currencies by name or symbol. * * @property searchRepository local search history storage * @property multiAccountStatusListSupplier supplier for loaded account status lists across all wallets @@ -37,16 +35,12 @@ class GetSearchResultsUseCase( * Produces a [Flow] of [SearchResult] for the given [query]. * * @param query the search query string; blank means "show history" - * @param marketTokens external flow of market token search results (provided by presentation layer) */ - operator fun invoke( - query: String, - marketTokens: Flow> = flowOf(emptyList()), - ): Flow { + operator fun invoke(query: String): Flow { return if (query.isBlank()) { observeHistory() } else { - searchAssets(query, marketTokens) + observeUserAssets(query) } } @@ -59,26 +53,11 @@ class GetSearchResultsUseCase( textHints = hints, recentTokens = tokens, userAssets = emptyList(), - marketTokens = emptyList(), ) } } - private fun searchAssets(query: String, marketTokens: Flow>): Flow { - return combine( - observeUserAssets(query), - marketTokens, - ) { userAssets, markets -> - SearchResult( - textHints = emptyList(), - recentTokens = emptyList(), - userAssets = userAssets, - marketTokens = markets, - ) - } - } - - private fun observeUserAssets(query: String): Flow> { + private fun observeUserAssets(query: String): Flow { val lowerQuery = query.lowercase() return combine( multiAccountStatusListSupplier(), @@ -94,6 +73,12 @@ class GetSearchResultsUseCase( statusLists .filter { it.userWalletId in unlockedWallets } .flatMap { statusList -> extractMatchingAssets(statusList, unlockedWallets, lowerQuery) } + }.map { userAssets -> + SearchResult( + textHints = emptyList(), + recentTokens = emptyList(), + userAssets = userAssets, + ) } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt index b37c5b7dc7..ac8f5fffdc 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt @@ -1,6 +1,8 @@ package com.tangem.features.feed.components.earn +import androidx.compose.animation.core.EaseOut import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -23,6 +25,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.components.haze.hazeEffectTangem import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.extensions.clickableSingle @@ -35,6 +38,7 @@ import com.tangem.features.feed.components.market.details.portfolio.add.AddToPor import com.tangem.features.feed.model.earn.EarnModel import com.tangem.features.feed.ui.components.FeedSearchBar import com.tangem.features.feed.ui.earn.EarnContent +import dev.chrisbanes.haze.HazeProgressive internal class DefaultEarnComponent( appComponentContext: AppComponentContext, @@ -59,7 +63,16 @@ internal class DefaultEarnComponent( FeedSearchBar( isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED, feedListSearchBar = state.feedListSearchBar, - modifier = Modifier.drawBehind { drawRect(background) }, + modifier = Modifier + .drawBehind { drawRect(background) } + .hazeEffectTangem { + progressive = HazeProgressive.verticalGradient( + startIntensity = .55f, + endIntensity = 0f, + preferPerformance = true, + easing = EaseOut, + ) + }, startContent = { Icon( imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28), @@ -93,11 +106,16 @@ internal class DefaultEarnComponent( } @Composable - override fun Content(bottomSheetState: State, modifier: Modifier) { + override fun Content( + bottomSheetState: State, + contentPadding: PaddingValues, + modifier: Modifier, + ) { val bottomSheet by bottomSheetSlot.subscribeAsState() val state by earnModel.state.collectAsStateWithLifecycle() EarnContent( + contentPadding = contentPadding, state = state, modifier = modifier, ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt index bd940eae65..4f06480af4 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt @@ -1,5 +1,6 @@ package com.tangem.features.feed.components.feed +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue @@ -18,14 +19,14 @@ import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.decompose.EmptyComposableBottomSheetComponent -import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles -import com.tangem.features.promobanners.api.PromoBannersBlockComponent import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent.Params import com.tangem.features.feed.model.feed.FeedComponentModel import com.tangem.features.feed.model.feed.FeedModelClickIntents import com.tangem.features.feed.ui.feed.FeedList import com.tangem.features.feed.ui.feed.FeedListHeader +import com.tangem.features.promobanners.api.NewPromoBannersFeatureToggles +import com.tangem.features.promobanners.api.PromoBannersBlockComponent internal class DefaultFeedComponent( appComponentContext: AppComponentContext, @@ -64,7 +65,11 @@ internal class DefaultFeedComponent( } @Composable - override fun Content(bottomSheetState: State, modifier: Modifier) { + override fun Content( + bottomSheetState: State, + contentPadding: PaddingValues, + modifier: Modifier, + ) { LifecycleStartEffect(Unit) { feedComponentModel.isVisibleOnScreen.value = true onStopOrDispose { @@ -78,6 +83,7 @@ internal class DefaultFeedComponent( modifier = modifier, state = state, promoBannersBlockComponent = promoBannersBlockComponent, + contentPadding = contentPadding, ) bottomSheet.child?.instance?.BottomSheet() } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt index dcf08d159a..3f9c69cdc3 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt @@ -1,6 +1,8 @@ package com.tangem.features.feed.components.market.details +import androidx.compose.animation.core.EaseOut import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -21,6 +23,7 @@ import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.components.haze.hazeEffectTangem import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.ds.topbar.TangemTopBar import com.tangem.core.ui.ds.topbar.TangemTopBarType @@ -37,6 +40,7 @@ import com.tangem.features.feed.model.market.details.analytics.MarketDetailsAnal import com.tangem.features.feed.model.market.details.state.TokenNetworksState import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsContent import com.tangem.features.feed.ui.market.detailed.MarketsTokenDetailsTopBar +import dev.chrisbanes.haze.HazeProgressive import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @@ -101,6 +105,14 @@ internal class DefaultMarketsTokenDetailsComponent( val background = LocalMainBottomSheetColor.current.value if (LocalRedesignEnabled.current) { TangemTopBar( + modifier = Modifier.hazeEffectTangem { + progressive = HazeProgressive.verticalGradient( + startIntensity = .55f, + endIntensity = 0f, + preferPerformance = true, + easing = EaseOut, + ) + }, startContent = { Icon( imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28), @@ -153,7 +165,11 @@ internal class DefaultMarketsTokenDetailsComponent( } @Composable - override fun Content(bottomSheetState: State, modifier: Modifier) { + override fun Content( + bottomSheetState: State, + contentPadding: PaddingValues, + modifier: Modifier, + ) { LifecycleStartEffect(Unit) { model.isVisibleOnScreen.value = true onStopOrDispose { @@ -167,6 +183,7 @@ internal class DefaultMarketsTokenDetailsComponent( } MarketsTokenDetailsContent( + contentPadding = contentPadding, modifier = modifier, backgroundColor = LocalMainBottomSheetColor.current.value, state = state, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/list/DefaultMarketsTokenListComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/list/DefaultMarketsTokenListComponent.kt index 81f649ec89..0fd788caa3 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/list/DefaultMarketsTokenListComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/list/DefaultMarketsTokenListComponent.kt @@ -1,6 +1,8 @@ package com.tangem.features.feed.components.market.list +import androidx.compose.animation.core.EaseOut import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -19,6 +21,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.components.haze.hazeEffectTangem import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.res.LocalMainBottomSheetColor @@ -31,6 +34,7 @@ import com.tangem.features.feed.model.market.list.state.SortByTypeUM import com.tangem.features.feed.ui.components.FeedSearchBar import com.tangem.features.feed.ui.market.list.MarketsList import com.tangem.features.feed.ui.market.list.TopBarWithSearch +import dev.chrisbanes.haze.HazeProgressive import kotlinx.serialization.Serializable internal class DefaultMarketsTokenListComponent( @@ -56,7 +60,16 @@ internal class DefaultMarketsTokenListComponent( FeedSearchBar( isSearchBarClickable = bottomSheetState.value == BottomSheetState.EXPANDED, feedListSearchBar = state.feedListSearchBar, - modifier = Modifier.drawBehind { drawRect(background) }, + modifier = Modifier + .drawBehind { drawRect(background) } + .hazeEffectTangem { + progressive = HazeProgressive.verticalGradient( + startIntensity = .55f, + endIntensity = 0f, + preferPerformance = true, + easing = EaseOut, + ) + }, startContent = { Icon( imageVector = ImageVector.vectorResource(id = R.drawable.ic_arrow_back_28), @@ -87,7 +100,11 @@ internal class DefaultMarketsTokenListComponent( } @Composable - override fun Content(bottomSheetState: State, modifier: Modifier) { + override fun Content( + bottomSheetState: State, + contentPadding: PaddingValues, + modifier: Modifier, + ) { LifecycleStartEffect(Unit) { model.isVisibleOnScreen.value = true onStopOrDispose { @@ -103,6 +120,7 @@ internal class DefaultMarketsTokenListComponent( } MarketsList( + contentPadding = contentPadding, modifier = modifier, state = state, ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt index 7b0b6465db..47732db33e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/details/DefaultNewsDetailsComponent.kt @@ -1,6 +1,8 @@ package com.tangem.features.feed.components.news.details +import androidx.compose.animation.core.EaseOut import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -18,6 +20,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.components.haze.hazeEffectTangem import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.ds.topbar.TangemTopBar import com.tangem.core.ui.ds.topbar.TangemTopBarType @@ -31,6 +34,7 @@ import com.tangem.domain.news.model.NewsListConfig import com.tangem.features.feed.model.news.details.NewsDetailsModel import com.tangem.features.feed.ui.news.details.NewsDetailsContent import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM +import dev.chrisbanes.haze.HazeProgressive import kotlinx.serialization.Serializable internal class DefaultNewsDetailsComponent( @@ -46,6 +50,14 @@ internal class DefaultNewsDetailsComponent( val state by newsDetailsModel.state.collectAsStateWithLifecycle() if (LocalRedesignEnabled.current) { TangemTopBar( + modifier = Modifier.hazeEffectTangem { + progressive = HazeProgressive.verticalGradient( + startIntensity = .55f, + endIntensity = 0f, + preferPerformance = true, + easing = EaseOut, + ) + }, type = TangemTopBarType.BottomSheet, startContent = { Icon( @@ -88,7 +100,6 @@ internal class DefaultNewsDetailsComponent( } else { TangemTopAppBar( containerColor = background, - title = null, startButton = TopAppBarButtonUM.Icon( iconRes = R.drawable.ic_back_24, onClicked = state.onBackClick, @@ -105,11 +116,16 @@ internal class DefaultNewsDetailsComponent( } @Composable - override fun Content(bottomSheetState: State, modifier: Modifier) { + override fun Content( + bottomSheetState: State, + contentPadding: PaddingValues, + modifier: Modifier, + ) { val state by newsDetailsModel.state.collectAsStateWithLifecycle() NewsDetailsContent( state = state, modifier = modifier, + contentPadding = contentPadding, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/list/DefaultNewsListComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/list/DefaultNewsListComponent.kt index 13505b2b0a..58a30e4a3a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/list/DefaultNewsListComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/news/list/DefaultNewsListComponent.kt @@ -1,6 +1,8 @@ package com.tangem.features.feed.components.news.list +import androidx.compose.animation.core.EaseOut import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -18,6 +20,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.components.haze.hazeEffectTangem import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.ds.topbar.TangemTopBar import com.tangem.core.ui.ds.topbar.TangemTopBarType @@ -30,6 +33,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.news.model.NewsListConfig import com.tangem.features.feed.model.news.list.NewsListModel import com.tangem.features.feed.ui.news.list.NewsListContent +import dev.chrisbanes.haze.HazeProgressive import kotlinx.serialization.Serializable internal class DefaultNewsListComponent( @@ -45,6 +49,14 @@ internal class DefaultNewsListComponent( val state by newsListModel.state.collectAsStateWithLifecycle() if (LocalRedesignEnabled.current) { TangemTopBar( + modifier = Modifier.hazeEffectTangem { + progressive = HazeProgressive.verticalGradient( + startIntensity = .55f, + endIntensity = 0f, + preferPerformance = true, + easing = EaseOut, + ) + }, title = resourceReference(R.string.common_news), type = TangemTopBarType.BottomSheet, startContent = { @@ -80,11 +92,16 @@ internal class DefaultNewsListComponent( } @Composable - override fun Content(bottomSheetState: State, modifier: Modifier) { + override fun Content( + bottomSheetState: State, + contentPadding: PaddingValues, + modifier: Modifier, + ) { val state by newsListModel.state.collectAsStateWithLifecycle() NewsListContent( state = state, modifier = modifier, + contentPadding = contentPadding, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt index b9cf5d8787..b8a8ac1119 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt @@ -1,5 +1,7 @@ package com.tangem.features.feed.components.search +import androidx.compose.animation.core.EaseOut +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -7,6 +9,7 @@ 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.state.BottomSheetState +import com.tangem.core.ui.components.haze.hazeEffectTangem import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.ds.field.search.TangemFieldShape import com.tangem.core.ui.ds.field.search.TangemSearchField @@ -15,6 +18,7 @@ import com.tangem.core.ui.ds.topbar.TangemTopBarType import com.tangem.features.feed.model.search.SearchModel import com.tangem.features.feed.ui.search.SearchContent import com.tangem.features.feed.ui.search.state.SearchCallbacks +import dev.chrisbanes.haze.HazeProgressive internal class DefaultSearchComponent( appComponentContext: AppComponentContext, @@ -35,6 +39,14 @@ internal class DefaultSearchComponent( } TangemTopBar( + modifier = Modifier.hazeEffectTangem { + progressive = HazeProgressive.verticalGradient( + startIntensity = .55f, + endIntensity = 0f, + preferPerformance = true, + easing = EaseOut, + ) + }, type = TangemTopBarType.BottomSheet, reserveSlotSpace = false, content = { @@ -50,7 +62,11 @@ internal class DefaultSearchComponent( } @Composable - override fun Content(bottomSheetState: State, modifier: Modifier) { + override fun Content( + bottomSheetState: State, + contentPadding: PaddingValues, + modifier: Modifier, + ) { val state by model.state.collectAsStateWithLifecycle() val searchCallbacks = remember { SearchCallbacks( @@ -64,6 +80,7 @@ internal class DefaultSearchComponent( modifier = modifier, content = state.content, searchCallbacks = searchCallbacks, + contentPadding = contentPadding, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt index fb91bf9fd9..a21b032562 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt @@ -34,6 +34,7 @@ import kotlinx.coroutines.launch import javax.inject.Inject private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L +private const val MARKET_SEARCH_DEBOUNCE_MS = 500L @Suppress("LongParameterList") @ModelScoped @@ -52,6 +53,7 @@ internal class SearchModel @Inject constructor( private val updateQuotesJob = JobHolder() private val searchResultsJob = JobHolder() + private val marketSearchDebounceJob = JobHolder() private var shouldShowAllTokensIncludingUnder100k = false private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency -> @@ -145,13 +147,20 @@ internal class SearchModel @Inject constructor( .onEach { query -> shouldShowAllTokensIncludingUnder100k = false if (query.isEmpty()) { + marketSearchDebounceJob.cancel() searchMarketsListManager.clearStateAndStopAllActions() updateQuotesJob.cancel() loadHistory() } else { stateController.update(SetSearchResultsLoadingTransformer()) - searchMarketsListManager.reload(searchText = query) subscribeToSearchResults(query) + modelScope.launch(dispatchers.default) { + delay(MARKET_SEARCH_DEBOUNCE_MS) + val latestQuery = stateController.value.searchBar.query.trim() + if (latestQuery.isNotEmpty()) { + searchMarketsListManager.reload(searchText = latestQuery) + } + }.saveIn(marketSearchDebounceJob) } } .launchIn(modelScope) @@ -159,10 +168,7 @@ internal class SearchModel @Inject constructor( private fun subscribeToSearchResults(query: String) { modelScope.launch { - getSearchResultsUseCase( - query = query, - marketTokens = searchMarketsListManager.rawItems, - ).collectLatest { searchResult -> + getSearchResultsUseCase(query = query).collectLatest { searchResult -> val userAssets = searchResult.userAssets.map { entry -> UserAssetItemUM( id = "${entry.userWalletId.stringValue}_${entry.accountId.value}" + diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt index 4f1527ad16..eaf7f1665d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt @@ -1,26 +1,30 @@ package com.tangem.features.feed.ui import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.* import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.remember +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.arkivanov.decompose.extensions.compose.stack.Children import com.arkivanov.decompose.router.stack.ChildStack import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.components.haze.hazeSourceTangem import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent +import com.tangem.core.ui.res.LocalHazeState import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.features.feed.components.FeedEntryChildFactory -import com.tangem.features.feed.ui.utils.contentFeedEntryAnimatedContentTransitionSpec +import com.tangem.features.feed.ui.utils.contentFeedEntryStackAnimation import com.tangem.features.feed.ui.utils.topBarFeedEntryAnimatedContentTransitionSpec +import dev.chrisbanes.haze.rememberHazeState @OptIn(ExperimentalDecomposeApi::class) @Composable @@ -29,18 +33,41 @@ internal fun EntryContent( stackState: State>, onHeaderSizeChange: (Dp) -> Unit, isOpenedInBottomSheet: Boolean, +) { + if (LocalRedesignEnabled.current) { + EntryContentV2( + bottomSheetState = bottomSheetState, + stackState = stackState, + onHeaderSizeChange = onHeaderSizeChange, + isOpenedInBottomSheet = isOpenedInBottomSheet, + ) + } else { + EntryContentV1( + bottomSheetState = bottomSheetState, + stackState = stackState, + onHeaderSizeChange = onHeaderSizeChange, + isOpenedInBottomSheet = isOpenedInBottomSheet, + ) + } +} + +@Composable +private fun EntryContentV1( + bottomSheetState: State, + stackState: State>, + onHeaderSizeChange: (Dp) -> Unit, + isOpenedInBottomSheet: Boolean, ) { val density = LocalDensity.current val background = LocalMainBottomSheetColor.current.value - val animationContent = remember(stackState) { contentFeedEntryAnimatedContentTransitionSpec(stackState) } - val animationAppBar = remember(stackState) { topBarFeedEntryAnimatedContentTransitionSpec(stackState) } + val stackAnimation = remember { contentFeedEntryStackAnimation() } Surface(contentColor = background) { Scaffold( containerColor = background, contentWindowInsets = WindowInsetsZero, topBar = { - AnimatedContent( + Children( modifier = Modifier .then( if (!isOpenedInBottomSheet) { @@ -56,6 +83,77 @@ internal fun EntryContent( } } }, + stack = stackState.value, + animation = stackAnimation, + ) { child -> + child.instance.Title(bottomSheetState) + } + }, + content = { contentPadding -> + Children( + stack = stackState.value, + animation = stackAnimation, + ) { child -> + child.instance.Content( + modifier = Modifier.padding(contentPadding), + bottomSheetState = bottomSheetState, + contentPadding = PaddingValues(), + ) + } + }, + ) + } +} + +@Composable +private fun EntryContentV2( + bottomSheetState: State, + stackState: State>, + onHeaderSizeChange: (Dp) -> Unit, + isOpenedInBottomSheet: Boolean, +) { + val density = LocalDensity.current + val background = LocalMainBottomSheetColor.current.value + val animationContent = remember { contentFeedEntryStackAnimation() } + val animationAppBar = remember(stackState) { topBarFeedEntryAnimatedContentTransitionSpec(stackState) } + var topBarHeight by remember { mutableStateOf(0.dp) } + val hazeState = rememberHazeState() + + Surface(contentColor = background) { + CompositionLocalProvider(LocalHazeState provides hazeState) { + Box(modifier = Modifier.fillMaxSize()) { + Children( + modifier = Modifier.fillMaxSize(), + stack = stackState.value, + animation = animationContent, + ) { child -> + child.instance.Content( + modifier = Modifier + .fillMaxSize() + .hazeSourceTangem(zIndex = 0f, state = hazeState), + contentPadding = PaddingValues(top = topBarHeight), + bottomSheetState = bottomSheetState, + ) + } + AnimatedContent( + modifier = Modifier + .align(Alignment.TopStart) + .then( + if (!isOpenedInBottomSheet) { + Modifier.statusBarsPadding() + } else { + Modifier + }, + ) + .onGloballyPositioned { coordinates -> + if (coordinates.size.height > 0) { + with(density) { + val height = coordinates.size.height.toDp() + topBarHeight = height + onHeaderSizeChange(height) + } + } + }, targetState = stackState.value.active, transitionSpec = animationAppBar, contentKey = { it.key }, @@ -63,20 +161,7 @@ internal fun EntryContent( ) { state -> state.instance.Title(bottomSheetState) } - }, - content = { contentPadding -> - AnimatedContent( - targetState = stackState.value.active, - transitionSpec = animationContent, - contentKey = { it.key }, - label = "FeedEntryContent", - ) { state -> - state.instance.Content( - modifier = Modifier.padding(contentPadding), - bottomSheetState = bottomSheetState, - ) - } - }, - ) + } + } } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt index 86ca75fa5b..d3800af21f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt @@ -22,11 +22,7 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.conditional -import com.tangem.core.ui.extensions.conditionalCompose -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.* import com.tangem.features.feed.ui.earn.components.* import com.tangem.features.feed.ui.earn.state.* @@ -36,7 +32,7 @@ import kotlinx.collections.immutable.persistentListOf private const val EARN_LOAD_MORE_BUFFER = 3 @Composable -internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { +internal fun EarnContent(contentPadding: PaddingValues, state: EarnUM, modifier: Modifier = Modifier) { val background = LocalMainBottomSheetColor.current.value val density = LocalDensity.current val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() } @@ -55,7 +51,7 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { modifier = modifier .fillMaxSize() .background(background), - contentPadding = PaddingValues(bottom = bottomBarHeight), + contentPadding = PaddingValues(bottom = bottomBarHeight, top = contentPadding.calculateTopPadding()), ) { item(key = "mostly_used_header") { SectionHeader( @@ -395,6 +391,7 @@ private fun EarnContentPreviewV1() { LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, ) { EarnContent( + contentPadding = PaddingValues(), state = previewEarnUM( mostlyUsed = EarnListUM.Content( items = persistentListOf( @@ -437,6 +434,7 @@ private fun EarnContentPreviewV2() { LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, ) { EarnContent( + contentPadding = PaddingValues(), state = previewEarnUM( mostlyUsed = EarnListUM.Content( items = persistentListOf( @@ -479,6 +477,7 @@ private fun EarnContentLoadingPreviewV1() { LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, ) { EarnContent( + contentPadding = PaddingValues(), state = previewEarnUM( mostlyUsed = EarnListUM.Error(onRetryClicked = {}), bestOpportunities = EarnBestOpportunitiesUM.Loading, @@ -498,6 +497,7 @@ private fun EarnContentLoadingPreviewV2() { LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, ) { EarnContent( + contentPadding = PaddingValues(), state = previewEarnUM( mostlyUsed = EarnListUM.Error(onRetryClicked = {}), bestOpportunities = EarnBestOpportunitiesUM.Loading, @@ -517,6 +517,7 @@ private fun EarnContentErrorPreviewV1() { LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, ) { EarnContent( + contentPadding = PaddingValues(), state = previewEarnUM( mostlyUsed = EarnListUM.Content( items = persistentListOf( @@ -545,6 +546,7 @@ private fun EarnContentErrorPreviewV2() { LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, ) { EarnContent( + contentPadding = PaddingValues(), state = previewEarnUM( mostlyUsed = EarnListUM.Content( items = persistentListOf( @@ -573,6 +575,7 @@ private fun EarnContentEmptyPreviewV1() { LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, ) { EarnContent( + contentPadding = PaddingValues(), state = previewEarnUM( mostlyUsed = EarnListUM.Content( items = persistentListOf( @@ -601,6 +604,7 @@ private fun EarnContentEmptyPreviewV2() { LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, ) { EarnContent( + contentPadding = PaddingValues(), state = previewEarnUM( mostlyUsed = EarnListUM.Content( items = persistentListOf( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt index 26f97c3c93..1c459a5e09 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt @@ -1,6 +1,7 @@ package com.tangem.features.feed.ui.feed import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.EaseOut import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith @@ -14,8 +15,11 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.haze.hazeEffectTangem import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.conditionalCompose import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.BaseSearchBarTestTags.SEARCH_BAR import com.tangem.features.feed.model.market.list.state.SortByTypeUM @@ -25,6 +29,7 @@ import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.crea import com.tangem.features.feed.ui.feed.state.FeedListSearchBar import com.tangem.features.feed.ui.feed.state.FeedListUM import com.tangem.features.feed.ui.feed.state.GlobalFeedState +import dev.chrisbanes.haze.HazeProgressive @Composable internal fun FeedListHeader( @@ -38,12 +43,26 @@ internal fun FeedListHeader( feedListSearchBar = feedListSearchBar, modifier = modifier .drawBehind { drawRect(background) } + .conditionalCompose( + condition = LocalRedesignEnabled.current, + modifier = { + hazeEffectTangem { + progressive = HazeProgressive.verticalGradient( + startIntensity = .75f, + endIntensity = 0f, + preferPerformance = true, + easing = EaseOut, + ) + } + }, + ) .testTag(SEARCH_BAR), ) } @Composable internal fun FeedList( + contentPadding: PaddingValues, state: FeedListUM, modifier: Modifier = Modifier, promoBannersBlockComponent: ComposableContentComponent? = null, @@ -59,19 +78,23 @@ internal fun FeedList( FeedListLoading( modifier = Modifier .fillMaxSize() - .verticalScroll(rememberScrollState()) - .drawBehind { drawRect(background) }, + .drawBehind { drawRect(background) } + .padding(top = contentPadding.calculateTopPadding()) + .verticalScroll(rememberScrollState()), ) } is GlobalFeedState.Error -> { FeedListGlobalError( onRetryClick = animatedState.onRetryClicked, - modifier = Modifier.drawBehind { drawRect(background) }, + modifier = Modifier + .drawBehind { drawRect(background) } + .padding(top = contentPadding.calculateTopPadding()), currentDate = state.currentDate, ) } is GlobalFeedState.Content -> { FeedListContent( + contentPadding = contentPadding, modifier = Modifier, state = state, promoBannersBlockComponent = promoBannersBlockComponent, @@ -83,6 +106,7 @@ internal fun FeedList( @Composable private fun FeedListContent( + contentPadding: PaddingValues, state: FeedListUM, modifier: Modifier = Modifier, promoBannersBlockComponent: ComposableContentComponent? = null, @@ -95,6 +119,9 @@ private fun FeedListContent( .padding(bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()) .drawBehind { drawRect(background) }, ) { + if (LocalRedesignEnabled.current) { + SpacerH(contentPadding.calculateTopPadding()) + } DateBlock(state.currentDate) SpacerH(32.dp) @@ -127,6 +154,6 @@ private fun FeedListContent( @Composable private fun FeedListPreview() { TangemThemePreview { - FeedList(state = createFeedPreviewState()) + FeedList(state = createFeedPreviewState(), contentPadding = PaddingValues()) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt index 8cbb7fb5e1..68076673c5 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt @@ -58,12 +58,14 @@ import com.tangem.core.ui.R as CoreR @Suppress("LongParameterList") @Composable internal fun MarketsTokenDetailsContent( + contentPadding: PaddingValues, state: MarketsTokenDetailsUM, backgroundColor: Color, modifier: Modifier = Modifier, portfolioBlock: @Composable ((Modifier) -> Unit)?, ) { Content( + contentPadding = contentPadding, modifier = modifier, backgroundColor = backgroundColor, state = state, @@ -80,6 +82,7 @@ internal fun MarketsTokenDetailsContent( @Suppress("LongParameterList") @Composable private fun Content( + contentPadding: PaddingValues, state: MarketsTokenDetailsUM, backgroundColor: Color, modifier: Modifier = Modifier, @@ -103,7 +106,7 @@ private fun Content( LazyColumn( state = lazyListState, - contentPadding = PaddingValues(bottom = bottomBarHeight), + contentPadding = PaddingValues(bottom = bottomBarHeight, top = contentPadding.calculateTopPadding()), ) { item("header") { Header( @@ -343,6 +346,7 @@ private fun MarketsTokenDetailsContent_Preview( state = params, backgroundColor = TangemTheme.colors.background.tertiary, portfolioBlock = {}, + contentPadding = PaddingValues(), ) } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/MarketsList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/MarketsList.kt index 8a1330df5d..cafab387b7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/MarketsList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/MarketsList.kt @@ -17,9 +17,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.common.ui.markets.preview.MarketChartListItemPreviewDataProvider -import com.tangem.core.ui.components.Keyboard -import com.tangem.core.ui.components.SpacerH12 -import com.tangem.core.ui.components.SpacerH8 +import com.tangem.core.ui.components.* import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState @@ -27,7 +25,6 @@ import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.TangemSearchBarDefaults import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.haze.hazeSourceTangem -import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.conditionalCompose import com.tangem.core.ui.extensions.resourceReference @@ -105,7 +102,7 @@ internal fun TopBarWithSearch( } @Composable -internal fun MarketsList(state: MarketsListUM, modifier: Modifier = Modifier) { +internal fun MarketsList(contentPadding: PaddingValues, state: MarketsListUM, modifier: Modifier = Modifier) { val background = LocalMainBottomSheetColor.current.value Column( modifier = modifier @@ -113,7 +110,7 @@ internal fun MarketsList(state: MarketsListUM, modifier: Modifier = Modifier) { .imePadding() .drawBehind { drawRect(background) }, ) { - Content(state = state) + Content(state = state, contentPadding = contentPadding) } MarketsListSortByBottomSheet(config = state.sortByBottomSheet) KeyboardEvents(isSortByBottomSheetShown = state.sortByBottomSheet.isShown) @@ -121,7 +118,7 @@ internal fun MarketsList(state: MarketsListUM, modifier: Modifier = Modifier) { @Suppress("LongMethod") @Composable -private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modifier) { +private fun ColumnScope.Content(contentPadding: PaddingValues, state: MarketsListUM, modifier: Modifier = Modifier) { val isRedesignEnabled = LocalRedesignEnabled.current val hazeState = rememberHazeState() @@ -135,6 +132,7 @@ private fun ColumnScope.Content(state: MarketsListUM, modifier: Modifier = Modif val scrolledState = remember { mutableStateOf(false) } Column(modifier.padding(horizontal = TangemTheme.dimens.size16)) { + SpacerH(contentPadding.calculateTopPadding()) AnimatedVisibility( visible = scrolledState.value.not() && state.isInSearchMode && @@ -276,6 +274,7 @@ private fun Preview() { LocalMainBottomSheetColor provides remember { mutableStateOf(primaryBackground) }, ) { MarketsList( + contentPadding = PaddingValues(), state = MarketsListUM( list = ListUM.Content( items = MarketChartListItemPreviewDataProvider().values diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt index e592094a1a..9b7905ade3 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt @@ -24,7 +24,7 @@ import com.tangem.features.feed.ui.news.details.state.MockArticlesFactory import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM @Composable -internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modifier) { +internal fun NewsDetailsContent(state: NewsDetailsUM, contentPadding: PaddingValues, modifier: Modifier = Modifier) { val background = LocalMainBottomSheetColor.current.value AnimatedContent( targetState = state.articlesStateUM, @@ -32,15 +32,16 @@ internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modif ) { animatedState -> when (animatedState) { ArticlesStateUM.Content -> { - Content(state = state, background = background) + Content(state = state, background = background, contentPadding = contentPadding) } ArticlesStateUM.Loading -> { - NewsDetailsPlaceholder(background = background) + NewsDetailsPlaceholder(background = background, contentPadding = contentPadding) } is ArticlesStateUM.LoadingError -> { Box( modifier = Modifier .fillMaxSize() + .padding(top = contentPadding.calculateTopPadding()) .background(background), contentAlignment = Alignment.Center, ) { @@ -57,7 +58,7 @@ internal fun NewsDetailsContent(state: NewsDetailsUM, modifier: Modifier = Modif } @Composable -private fun Content(state: NewsDetailsUM, background: Color) { +private fun Content(contentPadding: PaddingValues, state: NewsDetailsUM, background: Color) { val isRedesignEnabled = LocalRedesignEnabled.current val pagerState = rememberPagerState( initialPage = state.selectedArticleIndex, @@ -96,6 +97,7 @@ private fun Content(state: NewsDetailsUM, background: Color) { modifier = Modifier.fillMaxSize(), onLikeClick = { state.onLikeClick(article.id) }, relatedTokensUM = state.relatedTokensUM, + contentPadding = contentPadding, ) } if (state.articles.size > 1) { @@ -142,6 +144,7 @@ private fun PreviewNewsDetailsContent() { onBackClick = {}, onArticleIndexChanged = {}, ), + contentPadding = PaddingValues(), ) } } @@ -166,6 +169,7 @@ private fun PreviewNewsDetailsContentV2() { onBackClick = {}, onArticleIndexChanged = {}, ), + contentPadding = PaddingValues(), ) } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/ArticleDetail.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/ArticleDetail.kt index 12a7153f8d..7c7c47c46d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/ArticleDetail.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/ArticleDetail.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -28,6 +29,7 @@ import com.tangem.core.ui.ds.button.TangemButtonShape import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalHazeState import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme @@ -35,9 +37,11 @@ import com.tangem.features.feed.ui.feed.components.articles.ArticleHeader import com.tangem.features.feed.ui.news.details.state.ArticleUM import com.tangem.features.feed.ui.news.details.state.RelatedArticleUM import com.tangem.features.feed.ui.news.details.state.RelatedTokensUM +import dev.chrisbanes.haze.rememberHazeState @Composable internal fun ArticleDetail( + contentPadding: PaddingValues, article: ArticleUM, onLikeClick: () -> Unit, relatedTokensUM: RelatedTokensUM, @@ -45,6 +49,7 @@ internal fun ArticleDetail( ) { if (LocalRedesignEnabled.current) { ArticleDetailV2( + contentPadding = contentPadding, article = article, onLikeClick = onLikeClick, relatedTokensUM = relatedTokensUM, @@ -190,6 +195,7 @@ private fun ArticleDetailV1( @Suppress("LongMethod") @Composable internal fun ArticleDetailV2( + contentPadding: PaddingValues, article: ArticleUM, onLikeClick: () -> Unit, relatedTokensUM: RelatedTokensUM, @@ -199,126 +205,127 @@ internal fun ArticleDetailV2( val density = LocalDensity.current val background = LocalMainBottomSheetColor.current.value val pagerHeight = 32.dp - val contentPadding = pagerHeight + 56.dp + with(density) { + val bottomPadding = pagerHeight + 56.dp + with(density) { WindowInsets.navigationBars.getBottom(this).div(this.density) }.dp - Box(modifier = modifier) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .hazeSourceTangem(zIndex = 0f) - .background(background), - contentPadding = PaddingValues(bottom = contentPadding), - ) { - item("content") { - ArticleHeader( - title = article.title, - createdAt = article.createdAt.resolveReference(), - score = article.score, - tags = article.tags, - isTrending = article.isTrending, - modifier = Modifier - .padding(top = 16.dp) - .padding(horizontal = 16.dp), - ) - - if (article.shortContent.isNotEmpty()) { - QuickRecap( - content = article.shortContent, + CompositionLocalProvider(LocalHazeState provides rememberHazeState()) { + Box(modifier = modifier) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .hazeSourceTangem(zIndex = -1f) + .background(background), + contentPadding = PaddingValues(bottom = bottomPadding, top = contentPadding.calculateTopPadding()), + ) { + item("content") { + ArticleHeader( + title = article.title, + createdAt = article.createdAt.resolveReference(), + score = article.score, + tags = article.tags, + isTrending = article.isTrending, modifier = Modifier - .padding(top = 32.dp) + .padding(top = 16.dp) .padding(horizontal = 16.dp), ) - } - Text( - text = article.content, - style = TangemTheme.typography2.bodyRegular16, - color = TangemTheme.colors2.text.neutral.primary, - modifier = Modifier - .padding(top = 12.dp) - .padding(horizontal = 16.dp), - ) - - SpacerH(24.dp) - - HorizontalDivider( - modifier = Modifier.padding(horizontal = 24.dp), - color = TangemTheme.colors2.border.neutral.primary, - ) - - SpacerH(20.dp) - - SecondaryTangemButton( - modifier = Modifier.padding(horizontal = 24.dp), - text = resourceReference(R.string.news_like), - size = com.tangem.core.ui.ds.button.TangemButtonSize.X9, - iconRes = if (article.isLiked) { - R.drawable.ic_like_20 - } else { - R.drawable.ic_heart_20 - }, - onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - onLikeClick() - }, - shape = TangemButtonShape.Rounded, - ) - - RelatedTokensBlock( - relatedTokensUM = relatedTokensUM, - onItemClick = when (relatedTokensUM) { - is RelatedTokensUM.Content -> relatedTokensUM.onTokenClick - else -> null - }, - modifier = Modifier.padding(horizontal = 16.dp), - ) - - if (article.relatedArticles.isNotEmpty()) { - SpacerH(24.dp) - Row( - modifier = Modifier.padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = stringResourceSafe(R.string.news_sources), - style = TangemTheme.typography2.headingSemibold20, - color = TangemTheme.colors2.text.neutral.primary, + if (article.shortContent.isNotEmpty()) { + QuickRecap( + content = article.shortContent, + modifier = Modifier + .padding(top = 32.dp) + .padding(horizontal = 16.dp), ) } - } - } - if (article.relatedArticles.isNotEmpty()) { - item("relatedArticles") { - LazyRow( + Text( + text = article.content, + style = TangemTheme.typography2.bodyRegular16, + color = TangemTheme.colors2.text.neutral.primary, modifier = Modifier - .padding(vertical = 12.dp), - state = rememberLazyListState(), - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - items( - items = article.relatedArticles, - key = RelatedArticleUM::id, - ) { article -> - RelatedNewsItem( - relatedArticle = article, - modifier = Modifier.fillParentMaxHeight(), + .padding(top = 12.dp) + .padding(horizontal = 16.dp), + ) + + SpacerH(24.dp) + + HorizontalDivider( + modifier = Modifier.padding(horizontal = 24.dp), + color = TangemTheme.colors2.border.neutral.primary, + ) + + SpacerH(20.dp) + + SecondaryTangemButton( + modifier = Modifier.padding(horizontal = 24.dp), + text = resourceReference(R.string.news_like), + size = com.tangem.core.ui.ds.button.TangemButtonSize.X9, + iconRes = if (article.isLiked) { + R.drawable.ic_like_20 + } else { + R.drawable.ic_heart_20 + }, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + onLikeClick() + }, + shape = TangemButtonShape.Rounded, + ) + + RelatedTokensBlock( + relatedTokensUM = relatedTokensUM, + onItemClick = when (relatedTokensUM) { + is RelatedTokensUM.Content -> relatedTokensUM.onTokenClick + else -> null + }, + modifier = Modifier.padding(horizontal = 16.dp), + ) + + if (article.relatedArticles.isNotEmpty()) { + SpacerH(24.dp) + Row( + modifier = Modifier.padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResourceSafe(R.string.news_sources), + style = TangemTheme.typography2.headingSemibold20, + color = TangemTheme.colors2.text.neutral.primary, ) } } } - } - } - BottomFadeWithBlur( - modifier = Modifier - .align(Alignment.BottomCenter) - .height(80.dp) - .fillMaxWidth(), - backgroundColor = background, - ) + if (article.relatedArticles.isNotEmpty()) { + item("relatedArticles") { + LazyRow( + modifier = Modifier.padding(vertical = 12.dp), + state = rememberLazyListState(), + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + items( + items = article.relatedArticles, + key = RelatedArticleUM::id, + ) { article -> + RelatedNewsItem( + relatedArticle = article, + modifier = Modifier.fillParentMaxHeight(), + ) + } + } + } + } + } + + BottomFadeWithBlur( + modifier = Modifier + .align(Alignment.BottomCenter) + .height(80.dp) + .fillMaxWidth(), + backgroundColor = background, + ) + } } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/NewsDetailsPlaceholder.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/NewsDetailsPlaceholder.kt index a814d22eed..dc9107030f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/NewsDetailsPlaceholder.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/NewsDetailsPlaceholder.kt @@ -17,9 +17,9 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreviewRedesign @Composable -fun NewsDetailsPlaceholder(background: Color, modifier: Modifier = Modifier) { +fun NewsDetailsPlaceholder(contentPadding: PaddingValues, background: Color, modifier: Modifier = Modifier) { if (LocalRedesignEnabled.current) { - NewsDetailsPlaceholderV2(background, modifier) + NewsDetailsPlaceholderV2(contentPadding, background, modifier) } else { NewsDetailsPlaceholderV1(background, modifier) } @@ -103,13 +103,14 @@ private fun NewsDetailsPlaceholderV1(background: Color, modifier: Modifier = Mod @Suppress("LongMethod") @Composable -private fun NewsDetailsPlaceholderV2(background: Color, modifier: Modifier = Modifier) { +private fun NewsDetailsPlaceholderV2(contentPadding: PaddingValues, background: Color, modifier: Modifier = Modifier) { Column( modifier = modifier .fillMaxSize() .background(background) .padding(16.dp), ) { + SpacerH(contentPadding.calculateTopPadding()) Row( modifier = Modifier.height(50.dp), horizontalArrangement = Arrangement.spacedBy(30.dp), @@ -214,7 +215,10 @@ private fun NewsDetailsPlaceholderV2(background: Color, modifier: Modifier = Mod @Composable private fun NewsDetailsPlaceholderPreviewV1() { TangemThemePreview { - NewsDetailsPlaceholder(background = TangemTheme.colors.background.tertiary) + NewsDetailsPlaceholder( + background = TangemTheme.colors.background.tertiary, + contentPadding = PaddingValues(), + ) } } @@ -223,6 +227,9 @@ private fun NewsDetailsPlaceholderPreviewV1() { @Composable private fun NewsDetailsPlaceholderPreviewV2() { TangemThemePreviewRedesign { - NewsDetailsPlaceholder(background = TangemTheme.colors2.surface.level3) + NewsDetailsPlaceholder( + background = TangemTheme.colors2.surface.level3, + contentPadding = PaddingValues(), + ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt index ced6feef80..933d5fe2a9 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt @@ -12,7 +12,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.chip.Chip import com.tangem.core.ui.components.chip.entity.ChipUM @@ -22,6 +21,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.features.feed.ui.news.list.components.NewsListLazyColumn import com.tangem.features.feed.ui.news.list.state.NewsListState import com.tangem.features.feed.ui.news.list.state.NewsListUM @@ -29,7 +29,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableSet @Composable -internal fun NewsListContent(state: NewsListUM, modifier: Modifier = Modifier) { +internal fun NewsListContent(contentPadding: PaddingValues, state: NewsListUM, modifier: Modifier = Modifier) { val background = LocalMainBottomSheetColor.current.value val isRedesignEnabled = LocalRedesignEnabled.current val lazyListState = rememberLazyListState() @@ -39,6 +39,7 @@ internal fun NewsListContent(state: NewsListUM, modifier: Modifier = Modifier) { .fillMaxSize() .background(background), ) { + SpacerH(contentPadding.calculateTopPadding()) LazyRow( contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -149,6 +150,7 @@ private fun NewsListContentPreview() { onArticleClick = {}, onBackClick = {}, ), + contentPadding = PaddingValues(), ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt index d1f01ad1cc..b2da4c8e5f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt @@ -17,6 +17,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.vectorResource @@ -34,6 +35,7 @@ import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme import com.tangem.features.feed.ui.search.state.* @@ -41,17 +43,26 @@ private const val PLACEHOLDER_COUNT = 10 private const val LOAD_MORE_THRESHOLD = 5 @Composable -internal fun SearchContent(content: SearchContentUM, searchCallbacks: SearchCallbacks, modifier: Modifier = Modifier) { +internal fun SearchContent( + content: SearchContentUM, + searchCallbacks: SearchCallbacks, + contentPadding: PaddingValues, + modifier: Modifier = Modifier, +) { val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } val lazyListState = rememberLazyListState() + val background = LocalMainBottomSheetColor.current.value LazyColumn( state = lazyListState, - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .drawBehind { drawRect(background) }, contentPadding = PaddingValues( start = TangemTheme.dimens2.x4, end = TangemTheme.dimens2.x4, bottom = bottomBarHeight, + top = contentPadding.calculateTopPadding(), ), ) { when (content) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt index 227ab6c942..b8f37312a4 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt @@ -3,6 +3,7 @@ package com.tangem.features.feed.ui.search.preview import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -408,6 +409,7 @@ private fun SearchContentPreviewHost( content = scenario.content, searchCallbacks = SearchContentPreviewCallbacks, modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(), ) } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt index 02de097c9a..2bbfa496e2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt @@ -4,15 +4,16 @@ import androidx.compose.animation.* import androidx.compose.animation.core.tween import androidx.compose.runtime.State import com.arkivanov.decompose.Child +import com.arkivanov.decompose.FaultyDecomposeApi +import com.arkivanov.decompose.extensions.compose.stack.animation.StackAnimation +import com.arkivanov.decompose.extensions.compose.stack.animation.fade +import com.arkivanov.decompose.extensions.compose.stack.animation.slide +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.router.stack.ChildStack import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.features.feed.components.FeedEntryChildFactory -private fun FeedEntryChildFactory.Child?.usesFadeStackTransition(): Boolean = when (this) { - is FeedEntryChildFactory.Child.Search -> true - is FeedEntryChildFactory.Child.TokenList -> params.shouldAlwaysShowSearchBar - else -> false -} +private const val FEED_ENTRY_SLIDE_DURATION_MS = 300 internal typealias FeedEntryActiveChild = Child.Created @@ -20,12 +21,24 @@ internal typealias FeedEntryActiveChild = internal typealias FeedEntryChildStack = ChildStack -internal fun topBarFeedEntryAnimatedContentTransitionSpec( - stackState: State, -): AnimatedContentTransitionScope.() -> ContentTransform = - { feedEntryAnimatedContentTransform(stackState.value) } +@OptIn(FaultyDecomposeApi::class) +internal fun contentFeedEntryStackAnimation(): StackAnimation< + FeedEntryChildFactory.Child, + ComposableModularBottomSheetContentComponent, + > = + stackAnimation { to, from, _ -> + val isSearchToTokenList = + (to.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true + val isFromSearchTokenList = + (from.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true + if (isSearchToTokenList || isFromSearchTokenList) { + fade() + } else { + slide() + } + } -internal fun contentFeedEntryAnimatedContentTransitionSpec( +internal fun topBarFeedEntryAnimatedContentTransitionSpec( stackState: State, ): AnimatedContentTransitionScope.() -> ContentTransform = { feedEntryAnimatedContentTransform(stackState.value) } @@ -36,8 +49,8 @@ private fun AnimatedContentTransitionScope.feedEntryAnimat val shouldUseFade = initialState.configuration.usesFadeStackTransition() || targetState.configuration.usesFadeStackTransition() return if (shouldUseFade) { - fadeIn(animationSpec = tween(FEED_ENTRY_FADE_DURATION_MS)) togetherWith - fadeOut(animationSpec = tween(FEED_ENTRY_FADE_DURATION_MS)) + // No transition: fade/slide both fight with haze during the animation frame window. + EnterTransition.None togetherWith ExitTransition.None } else { feedEntrySlideTransform(stack) } @@ -66,5 +79,8 @@ private fun AnimatedContentTransitionScope.feedEntrySlideT } } -private const val FEED_ENTRY_FADE_DURATION_MS = 300 -private const val FEED_ENTRY_SLIDE_DURATION_MS = 300 \ No newline at end of file +private fun FeedEntryChildFactory.Child?.usesFadeStackTransition(): Boolean = when (this) { + is FeedEntryChildFactory.Child.Search -> true + is FeedEntryChildFactory.Child.TokenList -> params.shouldAlwaysShowSearchBar + else -> false +} \ No newline at end of file From c6621363038dfb50bbe6c933848543de91ab96bf Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Apr 2026 12:26:33 +0400 Subject: [PATCH 71/75] Updated on 2026-08-14 --- common/ui/build.gradle.kts | 1 + .../common/ui/tokens}/TokenConverterParams.kt | 5 +- .../common/ui/tokens/TokenItemGrouping.kt | 53 +++ .../java/com/tangem/utils/extensions/Map.kt | 15 +- .../MultiAccountStatusListSupplier.kt | 10 + .../status/utils}/ExpandedAccountsHolder.kt | 17 +- .../wallets/usecase/GetWalletsUseCase.kt | 12 + .../portfolio/add/AddToPortfolioComponent.kt | 1 + features/swap/impl/build.gradle.kts | 1 + .../feature/swap/DefaultSwapComponent.kt | 10 +- .../choosetoken/api/ChooseTokenComponent.kt | 36 +- .../impl/DefaultChooseTokenComponent.kt | 7 +- .../converter/ChooseTokenListItemConverter.kt | 140 +++++++ .../converter/SearchBarToggleTransformer.kt | 13 + .../SearchBarUpdateQueryTransformer.kt | 13 + .../impl/model/ChooseTokenModel.kt | 382 ++++++++---------- .../impl/model/MarketBlockDelegate.kt | 229 +++++++++++ .../impl/model/PortfolioListBlockDelegate.kt | 110 +++++ .../choosetoken/impl/ui/ChooseTokenScreen.kt | 83 ++-- .../swap/choosetoken/impl/ui/ChooseTokenUM.kt | 16 +- .../account/AccountDependencies.kt | 1 + .../transformers/SetTokenListTransformer.kt | 1 + .../converter/TokenListStateConverter.kt | 59 +-- .../YieldSupplyPromoBannerConverter.kt | 2 +- .../subscribers/BasicAccountListSubscriber.kt | 9 +- .../YieldSupplyPromoBannerConverterTest.kt | 26 +- 26 files changed, 906 insertions(+), 346 deletions(-) rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers => common/ui/src/main/java/com/tangem/common/ui/tokens}/TokenConverterParams.kt (80%) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemGrouping.kt rename {features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account => domain/account/status/src/main/java/com/tangem/domain/account/status/utils}/ExpandedAccountsHolder.kt (91%) create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/ChooseTokenListItemConverter.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/SearchBarToggleTransformer.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/SearchBarUpdateQueryTransformer.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt create mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/PortfolioListBlockDelegate.kt diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index 6e2c8833bb..71620a30d6 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { implementation(projects.domain.card) implementation(projects.domain.staking.models) implementation(projects.domain.staking) + implementation(projects.domain.account) implementation(projects.domain.tokens.models) implementation(projects.domain.transaction.models) implementation(projects.domain.wallets.models) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TokenConverterParams.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenConverterParams.kt similarity index 80% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TokenConverterParams.kt rename to common/ui/src/main/java/com/tangem/common/ui/tokens/TokenConverterParams.kt index 03ffab9a92..e6541f20cb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TokenConverterParams.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenConverterParams.kt @@ -1,13 +1,14 @@ -package com.tangem.feature.wallet.presentation.wallet.state.transformers +package com.tangem.common.ui.tokens import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.tokenlist.TokenList sealed interface TokenConverterParams { /** Wallet mode; list of tokens for main account */ data class Wallet( - val accountId: AccountId, + val mainAccount: AccountStatus, val tokenList: TokenList, ) : TokenConverterParams diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemGrouping.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemGrouping.kt new file mode 100644 index 0000000000..f2b67b9645 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemGrouping.kt @@ -0,0 +1,53 @@ +package com.tangem.common.ui.tokens + +import com.tangem.common.ui.R +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup + +object TokenItemGrouping { + + fun TokenList.GroupedByNetwork.toGroupedItems(tokenConverter: TokenItemStateConverter): List { + return groups.fold(initial = mutableListOf()) { acc, group -> + acc.addGroup(tokenConverter, group) + } + } + + fun TokenList.Ungrouped.toUngroupedItems(tokenConverter: TokenItemStateConverter): List { + return currencies.fold(initial = mutableListOf()) { acc, token -> + acc.addToken(tokenConverter, token) + } + } + + fun MutableList.addGroup( + tokenConverter: TokenItemStateConverter, + group: NetworkGroup, + ): MutableList { + val groupTitle = TokensListItemUM.GroupTitle( + id = group.network.hashCode(), + text = resourceReference( + id = R.string.wallet_network_group_title, + formatArgs = wrappedList(group.network.name), + ), + ) + + add(groupTitle) + group.currencies.forEach { token -> addToken(tokenConverter, token) } + + return this + } + + fun MutableList.addToken( + tokenConverter: TokenItemStateConverter, + token: CryptoCurrencyStatus, + ): MutableList { + val tokenItemState = tokenConverter.convert(token) + + add(TokensListItemUM.Token(tokenItemState)) + + return this + } +} \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Map.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Map.kt index 99cf375010..dd40d06523 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/Map.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/Map.kt @@ -1,11 +1,10 @@ package com.tangem.utils.extensions -fun Map.mapNotNullValues(transform: (Map.Entry) -> R?): Map { - return this - .mapNotNull { entry -> - val newValue = transform(entry) ?: return@mapNotNull null - - entry.key to newValue - } - .toMap() +inline fun Map.mapNotNullValues(transform: (Map.Entry) -> R?): Map { + val result = linkedMapOf() + this.forEach { entry -> + val newValue = transform(entry) ?: return@forEach + result[entry.key] = newValue + } + return result } \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/supplier/MultiAccountStatusListSupplier.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/supplier/MultiAccountStatusListSupplier.kt index b37d457efc..559ab0894e 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/supplier/MultiAccountStatusListSupplier.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/supplier/MultiAccountStatusListSupplier.kt @@ -3,7 +3,9 @@ package com.tangem.domain.account.status.supplier import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.producer.MultiAccountStatusListProducer import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map /** * Supplier that provides a list of [AccountStatusList]s for all user wallets. @@ -18,4 +20,12 @@ abstract class MultiAccountStatusListSupplier( operator fun invoke(): Flow> { return super.invoke(params = Unit) } + + fun invokeAsMap(): Flow> = invoke() + .map { accountLists -> + accountLists.associateByTo( + destination = linkedMapOf(), + keySelector = { accountList -> accountList.userWalletId }, + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/ExpandedAccountsHolder.kt similarity index 91% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt rename to domain/account/status/src/main/java/com/tangem/domain/account/status/utils/ExpandedAccountsHolder.kt index b1c48cff7e..4a33fdf3b8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/ExpandedAccountsHolder.kt @@ -1,6 +1,5 @@ -package com.tangem.feature.wallet.presentation.account +package com.tangem.domain.account.status.utils -import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.account.models.AccountExpandedState import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.repository.AccountsExpandedRepository @@ -16,9 +15,11 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +import javax.inject.Singleton -@ModelScoped -internal class ExpandedAccountsHolder @Inject constructor( +// todo swap separate for main and swap +@Singleton +class ExpandedAccountsHolder @Inject constructor( private val singleAccountListSupplier: SingleAccountListSupplier, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val accountsExpandedRepository: AccountsExpandedRepository, @@ -30,9 +31,9 @@ internal class ExpandedAccountsHolder @Inject constructor( onBufferOverflow = BufferOverflow.DROP_OLDEST, ) - fun expandedAccounts(userWallet: UserWallet): Flow> = channelFlow { - val walletId = userWallet.walletId + fun expandedAccounts(userWallet: UserWallet): Flow> = expandedAccounts(userWallet.walletId) + fun expandedAccounts(walletId: UserWalletId): Flow> = channelFlow { val storedState = accountsExpandedRepository.expandedAccounts .map { it[walletId].orEmpty() } .stateIn(this) @@ -65,7 +66,7 @@ internal class ExpandedAccountsHolder @Inject constructor( walletAccounts(walletId).onEach { accountList -> if (!isAccountsModeEnabledUseCase.invokeSync()) { accountsExpandedRepository.clearStore() - expandedAccounts.update { setOf() } + expandedAccounts.update { emptySet() } return@onEach } val idsSet = accountList.accounts.mapTo(mutableSetOf()) { it.accountId } @@ -88,7 +89,7 @@ internal class ExpandedAccountsHolder @Inject constructor( if (isAccountMode) { channel.send(expanded) } else { - channel.send(setOf()) + channel.send(emptySet()) } }, ).collect() diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt index de1b19acf0..1a436c9f93 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt @@ -2,8 +2,10 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import java.util.LinkedHashMap /** * Use case for getting list of user wallets @@ -19,6 +21,16 @@ class GetWalletsUseCase( @Throws(IllegalArgumentException::class) operator fun invoke(): Flow> = userWalletsListRepository.userWallets.map { requireNotNull(it) } + @Throws(IllegalArgumentException::class) + fun invokeAsMap(): Flow> = userWalletsListRepository.userWallets + .map { requireNotNull(it) } + .map { wallets -> + wallets.associateByTo( + destination = linkedMapOf(), + keySelector = { wallet -> wallet.walletId }, + ) + } + @Throws(IllegalArgumentException::class) fun invokeSync(): List = userWalletsListRepository.userWallets.value!! } \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioComponent.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioComponent.kt index 902f38339d..5a1dabaa43 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioComponent.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioComponent.kt @@ -14,6 +14,7 @@ interface AddToPortfolioComponent : ComposableBottomSheetComponent { interface Callback { fun onDismiss() + // todo swap add new onSuccess with full data of added token fun onSuccess(addedToken: CryptoCurrency) } diff --git a/features/swap/impl/build.gradle.kts b/features/swap/impl/build.gradle.kts index 297b57d054..6cd1167b8e 100644 --- a/features/swap/impl/build.gradle.kts +++ b/features/swap/impl/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { /** Domain modules **/ implementation(projects.domain.models) + implementation(projects.domain.account) implementation(projects.domain.appCurrency) implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index e35364d0e5..27f301ffb3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -12,6 +12,7 @@ import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.essenty.lifecycle.subscribe import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState +import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext @@ -21,6 +22,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent import com.tangem.feature.swap.model.SwapModel @@ -52,7 +54,13 @@ internal class DefaultSwapComponent @AssistedInject constructor( private val chooseTokenComponent by lazy { chooseTokenComponentFactory.create( context = child("chooseTokenComponent"), - params = ChooseTokenComponent.Params(model.chooseTokenBridge), + params = ChooseTokenComponent.Params( + bridge = model.chooseTokenBridge, + settings = ChooseTokenComponent.Settings.SwapTo, + analyticsPayload = setOf( + ChooseTokenAnalyticsPayload.ScreensSources(ScreensSources.Swap.value), + ), + ), ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt index 9caef19998..e87799d55e 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt @@ -2,12 +2,14 @@ package com.tangem.feature.swap.choosetoken.api import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus -import com.tangem.domain.models.account.Account +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup +import com.tangem.feature.swap.presentation.R import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow @@ -56,13 +58,12 @@ internal interface ChooseTokenBridge { } data class ChooseTokenResult( - val addedCurrency: AccountCryptoCurrencyStatus, - val userWallet: UserWallet, + val currency: CryptoCurrencyStatus, + val account: AccountStatus, + val wallet: UserWallet, val analyticsPayload: Set = emptySet(), ) { - val status: CryptoCurrencyStatus get() = addedCurrency.status - val account: Account.CryptoPortfolio get() = addedCurrency.account - val walletId get() = userWallet.walletId + val walletId get() = wallet.walletId } sealed interface ChooseTokenAnalyticsPayload { @@ -70,13 +71,34 @@ sealed interface ChooseTokenAnalyticsPayload { @Suppress("BooleanPropertyNaming") @JvmInline value class IsSearched(val value: Boolean) : ChooseTokenAnalyticsPayload + + @JvmInline + value class ScreensSources(val value: String) : ChooseTokenAnalyticsPayload } internal interface ChooseTokenComponent : ComposableContentComponent { data class Params( val bridge: ChooseTokenBridge, + val settings: Settings, + val analyticsPayload: Set = emptySet(), ) + data class Settings( + val title: TextReference, + val isShowMarketBlock: Boolean, + ) { + companion object { + val SwapFrom = Settings( + title = resourceReference(R.string.swapping_from_title), + isShowMarketBlock = false, + ) + val SwapTo = Settings( + title = resourceReference(R.string.swapping_to_title), + isShowMarketBlock = true, + ) + } + } + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt index 3ac09c489f..62bd2f26e5 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt @@ -38,11 +38,14 @@ internal class DefaultChooseTokenComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { - val state by model.state.collectAsStateWithLifecycle() + val stateOld by model.stateOld.collectAsStateWithLifecycle() val bottomSheet by bottomSheetSlot.subscribeAsState() - state?.let { stateHolder -> + stateOld?.let { stateHolder -> SwapSelectTokenScreen(state = stateHolder, onBack = { model.onBackClicked() }) } + // todo swap uncomment + // val state by model.state.collectAsStateWithLifecycle() + // ChooseTokenScreen(state = state) bottomSheet.child?.instance?.BottomSheet() } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/ChooseTokenListItemConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/ChooseTokenListItemConverter.kt new file mode 100644 index 0000000000..a130a0eabf --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/ChooseTokenListItemConverter.kt @@ -0,0 +1,140 @@ +package com.tangem.feature.swap.choosetoken.impl.converter + +import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +import com.tangem.common.ui.account.TokensListPortfolioItemConverter +import com.tangem.common.ui.tokens.TokenConverterParams +import com.tangem.common.ui.tokens.TokenItemGrouping.toGroupedItems +import com.tangem.common.ui.tokens.TokenItemGrouping.toUngroupedItems +import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.feature.swap.choosetoken.impl.model.ClickIntents +import com.tangem.feature.swap.choosetoken.impl.model.isSearchingState +import com.tangem.feature.swap.models.TokenListUMData +import kotlinx.collections.immutable.toPersistentList + +internal class ChooseTokenListItemConverter( + private val appCurrency: AppCurrency, + private val params: TokenConverterParams, + private val clickIntents: ClickIntents, + private val searchQuery: String, +) { + + private val isSearchingState: Boolean get() = searchQuery.isSearchingState + + private val onTokenClick: (account: AccountStatus, currencyStatus: CryptoCurrencyStatus) -> Unit = + { account, currencyStatus -> + clickIntents.onTokenItemClick(account, currencyStatus) + } + + private fun tokenStatusConverter(account: AccountStatus) = TokenItemStateConverter( + appCurrency = appCurrency, + onItemClick = { _, status -> onTokenClick(account, status) }, + ) + + fun convert(): TokenListUMData { + return when (params) { + is TokenConverterParams.Account -> convertAccountList(params) + is TokenConverterParams.Wallet -> convertTokenList( + tokenConverter = tokenStatusConverter(params.mainAccount), + tokenListParam = params.tokenList, + ) + } + } + + private fun convertAccountList(params: TokenConverterParams.Account): TokenListUMData { + val accountList = params.accountList + val accountItems = accountList.accountStatuses + .filterCryptoPortfolio() + .map { accountStatus -> accountStatus.toPortfolioItem(params) } + .filter { portfolio -> portfolio.tokens.isNotEmpty() } + if (accountItems.isEmpty()) { + return TokenListUMData.EmptyList + } + val accountsList = accountItems.toPersistentList() + return TokenListUMData.AccountList( + tokensList = accountsList, + totalTokensCount = accountsList.size, + ) + } + + private fun AccountStatus.CryptoPortfolio.toPortfolioItem( + params: TokenConverterParams.Account, + ): TokensListItemUM.Portfolio { + val tokenList: TokenList = this.tokenList + val account: Account.CryptoPortfolio = this.account + val isExpanded = isSearchingState || params.expandedAccounts.contains(account.accountId) + val onItemClick: (Account.CryptoPortfolio) -> Unit = { clickedAccount -> + if (isExpanded) { + clickIntents.onAccountCollapseClick(clickedAccount) + } else { + clickIntents.onAccountExpandClick(clickedAccount) + } + } + val converter = AccountCryptoPortfolioItemStateConverter( + appCurrency = appCurrency, + account = account, + onItemClick = onItemClick.takeIf { !isSearchingState }, + priceChangeLce = this.priceChangeLce, + ) + val accountItem = converter.convert(tokenList.totalFiatBalance) + val tokenConverter = tokenStatusConverter(this) + val tokensListState = convertTokenList(tokenConverter, tokenList) + val items = tokensListState.tokensList + return TokensListPortfolioItemConverter( + tokenItemUM = accountItem, + isExpanded = isExpanded, + isCollapsable = !isSearchingState, + tokens = items.filterIsInstance().toPersistentList(), + ).convert(Unit) + } + + private fun convertTokenList(tokenConverter: TokenItemStateConverter, tokenListParam: TokenList): TokenListUMData { + val tokenList = if (isSearchingState) filterByQuery(tokenListParam) else tokenListParam + + return when (tokenList) { + is TokenList.Empty -> TokenListUMData.EmptyList + is TokenList.GroupedByNetwork -> tokenList.toGroupedItems(tokenConverter).let { grouped -> + TokenListUMData.TokenList( + tokensList = grouped.toPersistentList(), + totalTokensCount = grouped.size, + ) + } + is TokenList.Ungrouped -> tokenList.toUngroupedItems(tokenConverter).let { ungrouped -> + TokenListUMData.TokenList( + tokensList = ungrouped.toPersistentList(), + totalTokensCount = ungrouped.size, + ) + } + } + } + + private fun filterByQuery(tokenList: TokenList): TokenList { + fun List.filterByQuery(): List = filter { currency -> + currency.currency.name.contains(searchQuery, ignoreCase = true) || + currency.currency.symbol.contains(searchQuery, ignoreCase = true) + } + return when (tokenList) { + TokenList.Empty -> TokenList.Empty + is TokenList.Ungrouped -> { + val filtered = tokenList.currencies.filterByQuery() + if (filtered.isEmpty()) TokenList.Empty else tokenList.copy(currencies = filtered) + } + is TokenList.GroupedByNetwork -> { + val filteredGroups = tokenList.groups + .map { group -> + val filteredCurrencies = group.currencies.filterByQuery() + group.copy(currencies = filteredCurrencies) + } + .filter { group -> group.currencies.isNotEmpty() } + if (filteredGroups.isEmpty()) TokenList.Empty else tokenList.copy(groups = filteredGroups) + } + } + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/SearchBarToggleTransformer.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/SearchBarToggleTransformer.kt new file mode 100644 index 0000000000..94dc7a09a9 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/SearchBarToggleTransformer.kt @@ -0,0 +1,13 @@ +package com.tangem.feature.swap.choosetoken.impl.converter + +import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenInitialUM +import com.tangem.utils.transformer.Transformer + +internal class SearchBarToggleTransformer(private val isActive: Boolean) : Transformer { + + override fun transform(prevState: ChooseTokenInitialUM): ChooseTokenInitialUM { + return prevState.copy( + searchBar = prevState.searchBar.copy(isActive = isActive), + ) + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/SearchBarUpdateQueryTransformer.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/SearchBarUpdateQueryTransformer.kt new file mode 100644 index 0000000000..33cf694057 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/SearchBarUpdateQueryTransformer.kt @@ -0,0 +1,13 @@ +package com.tangem.feature.swap.choosetoken.impl.converter + +import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenInitialUM +import com.tangem.utils.transformer.Transformer + +internal class SearchBarUpdateQueryTransformer(private val newQuery: String) : Transformer { + + override fun transform(prevState: ChooseTokenInitialUM): ChooseTokenInitialUM { + return prevState.copy( + searchBar = prevState.searchBar.copy(query = newQuery), + ) + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt index 7d7812024f..2eb003236d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt @@ -1,112 +1,118 @@ package com.tangem.feature.swap.choosetoken.impl.model -import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.common.ui.markets.models.MarketsListItemUM -import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.ui.R -import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains -import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketListConfig -import com.tangem.domain.markets.toSerializableParam +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload -import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge -import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent -import com.tangem.feature.swap.choosetoken.api.SettingContextUseCase +import com.tangem.feature.swap.choosetoken.api.* +import com.tangem.feature.swap.choosetoken.impl.converter.SearchBarToggleTransformer +import com.tangem.feature.swap.choosetoken.impl.converter.SearchBarUpdateQueryTransformer +import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenFullUM +import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenInitialUM +import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenUM +import com.tangem.feature.swap.choosetoken.impl.ui.WalletListUM import com.tangem.feature.swap.converters.TokensDataConverter -import com.tangem.feature.swap.models.AddToPortfolioRoute import com.tangem.feature.swap.models.SwapSelectTokenStateHolder -import com.tangem.feature.swap.models.market.MarketsListBatchFlowManager +import com.tangem.feature.swap.models.TokenListUMData import com.tangem.feature.swap.models.market.state.SwapMarketState +import com.tangem.feature.swap.presentation.R import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager -import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import javax.inject.Inject +internal val String.isSearchingState: Boolean get() = this.isNotBlank() +internal val StateFlow.isSearchingState: Boolean get() = this.value.isSearchingState + @Suppress("LongParameterList") @ModelScoped internal class ChooseTokenModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, - private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, - private val excludedBlockchains: ExcludedBlockchains, - private val getUserWalletsUseCase: GetWalletsUseCase, private val settingContextUseCase: SettingContextUseCase, - private val marketsListBatchFlowManagerFactory: MarketsListBatchFlowManager.Factory, + private val getWalletsUseCase: GetWalletsUseCase, + portfolioListBlockDelegateFactory: PortfolioListBlockDelegate.Factory, + marketBlockDelegateFactory: MarketBlockDelegate.Factory, paramsContainer: ParamsContainer, ) : Model() { private val params = paramsContainer.require() private val bridge: ChooseTokenBridge = params.bridge - private val searchQueryState = bridge.searchQueryState - private val addToPortfolioJobHolder = JobHolder() + private val searchQueryState: StateFlow = bridge.searchQueryState + private val isSearchingState: Boolean get() = bridge.searchQueryState.isSearchingState + private val marketBlockDelegate: MarketBlockDelegate = marketBlockDelegateFactory.create( + modelScope = modelScope, + searchQueryState = searchQueryState, + screensSourcesName = params.analyticsPayload + .filterIsInstance() + .firstOrNull()?.value.orEmpty(), + ) + private val portfolioListBlockDelegate: PortfolioListBlockDelegate = portfolioListBlockDelegateFactory.create( + modelScope = modelScope, + searchQueryState = searchQueryState, + ) - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - - private val visibleMarketItemIds = MutableStateFlow>(emptyList()) - private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) + val bottomSheetNavigation get() = marketBlockDelegate.addToPortfolioSlot + val addToPortfolioManager get() = marketBlockDelegate.addToPortfolioManager + private val marketsStateFlow: Flow = if (params.settings.isShowMarketBlock) { + marketBlockDelegate.marketsStateFlow + } else { + flowOf(null) + } private val expandedAccountsFlow: MutableStateFlow> = MutableStateFlow(emptyMap()) + private val onWalletSelected = Channel() - var addToPortfolioManager: AddToPortfolioManager? = null + // todo swap call new api result val addToPortfolioCallback = object : AddToPortfolioComponent.Callback { - override fun onDismiss() = bottomSheetNavigation.dismiss() + override fun onDismiss() = marketBlockDelegate.addToPortfolioSlot.dismiss() override fun onSuccess(addedToken: CryptoCurrency) { - modelScope.launch { - val newToken = addedToken to ChooseTokenAnalyticsPayload - .IsSearched(searchQueryState.value.isNotEmpty()) - bridge.onNewTokenAdded(newToken) - bottomSheetNavigation.dismiss() - } + val newToken = addedToken to ChooseTokenAnalyticsPayload.IsSearched(isSearchingState) + bridge.onNewTokenAdded(newToken) + marketBlockDelegate.addToPortfolioSlot.dismiss() } } - private val defaultMarketsListManager by lazy { - marketsListBatchFlowManagerFactory.create( - batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, - order = TokenMarketListConfig.Order.Trending, - currentSearchText = Provider { null }, - modelScope = modelScope, - ) - } + val stateOld: StateFlow = combineUIOld() - private val searchMarketsListManager by lazy { - marketsListBatchFlowManagerFactory.create( - batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, - order = TokenMarketListConfig.Order.ByRating, - currentSearchText = Provider { searchQueryState.value }, - modelScope = modelScope, - ) - } + private val contentState: StateFlow = combineUI() + private val initialState: MutableStateFlow = MutableStateFlow(getInitState()) + val state: StateFlow = combine( + flow = initialState, + flow2 = contentState, + transform = { initial, content -> + ChooseTokenFullUM( + initialUM = initial, + contentUM = content, + ) + }, + ).stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = ChooseTokenFullUM(initialState.value, contentState.value), + ) - val state: StateFlow = combineUI() - - init { - subscribeMarketTokens() - } - - private fun combineUI(): StateFlow = combine( + @Suppress("UnusedPrivateMember") + private fun combineUIOld(): StateFlow = combine( flow = bridge.currenciesGroup, flow2 = settingContextUseCase.invoke(), - flow3 = marketsStateFlow(), + flow3 = marketsStateFlow, flow4 = expandedAccountsFlow, transform = { currenciesGroup, settingContext, marketState, expandedAccounts -> val isAccountsMode = settingContext.isAccountsMode @@ -117,7 +123,7 @@ internal class ChooseTokenModel @Inject constructor( onSearchEntered = { query -> bridge.onSearchQuery(query) }, onTokenClick = { tokenId -> val selected = tokenId to ChooseTokenAnalyticsPayload - .IsSearched(searchQueryState.value.isNotEmpty()) + .IsSearched(isSearchingState) bridge.onTokenSelected(selected) }, onAccountClick = { account -> @@ -132,170 +138,124 @@ internal class ChooseTokenModel @Inject constructor( isBalanceHidden = isBalanceHidden, isAccountsMode = isAccountsMode, appCurrency = appCurrency, - marketState = marketState, + marketState = requireNotNull(marketState), ).transform() }, ) .flowOn(dispatchers.default) .stateIn(modelScope, SharingStarted.Eagerly, initialValue = null) - private fun marketsStateFlow() = searchQueryState - // Switch between default and search market flows - .map { it.isEmpty() } - .distinctUntilChanged() - .flatMapLatest { isDefaultMode -> - if (isDefaultMode) { - visibleMarketItemIds.value = emptyList() - createDefaultMarketsFlow() - } else { - visibleDefaultMarketItemIds.value = emptyList() - createSearchMarketsFlow() - } - } + @Suppress("LongMethod") + private fun combineUI(): StateFlow = channelFlow { + val allWalletsFlow: StateFlow> = + getWalletsUseCase.invokeAsMap().stateIn(this) - private fun subscribeMarketTokens() { - // Reload search markets when query changes - searchQueryState - .onEach { searchQuery -> - if (searchQuery.isNotEmpty()) { - searchMarketsListManager.reload(searchQuery) - } - } - .launchIn(modelScope) + val selectedWalletFlow: StateFlow = + onWalletSelected.receiveAsFlow() + .mapNotNull { walletId -> allWalletsFlow.value[walletId] } + .stateIn(this, SharingStarted.Eagerly, allWalletsFlow.value.values.first()) - // Initial load of default markets - defaultMarketsListManager.reload() - - visibleMarketItemIds - .mapNotNull { rawIDS -> - if (rawIDS.isNotEmpty()) { - searchMarketsListManager.getBatchKeysByItemIds(rawIDS) - } else { - null - } - } + val selectedWalletTokensData: Flow = combine( + flow = selectedWalletFlow.map { wallet -> wallet.walletId }.distinctUntilChanged(), + flow2 = portfolioListBlockDelegate.portfolioList, + transform = { selectedWalletId, allPortfoliosData -> allPortfoliosData[selectedWalletId] }, + ) + .filterNotNull() .distinctUntilChanged() - .transformLatest, Unit> { visibleBatchKeys -> - searchMarketsListManager.loadCharts(visibleBatchKeys) - } - .launchIn(modelScope) - visibleDefaultMarketItemIds - .mapNotNull { rawIds -> - if (rawIds.isNotEmpty()) { - defaultMarketsListManager.getBatchKeysByItemIds(rawIds) + val walletListUmFlow = combine( + flow = selectedWalletFlow, + flow2 = allWalletsFlow, + transform = { selectedWallet, allWallets -> + allWallets.entries + .map { (walletId, wallet) -> + val type = if (selectedWallet.walletId == walletId) { + TangemButtonType.Primary + } else { + TangemButtonType.Secondary + } + TangemButtonUM( + text = stringReference(wallet.name), + onClick = { onWalletSelected.trySend(walletId) }, + type = type, + ) + } + }, + ) + .distinctUntilChanged() + + portfolioListBlockDelegate.onTokenItemClick.receiveAsFlow() + .onEach { (account, currencyStatus) -> + onTokenItemClick( + wallet = allWalletsFlow.value[account.accountId.userWalletId] ?: return@onEach, + account = account, + currencyStatus = currencyStatus, + ) + } + .launchIn(this) + + combine( + flow = selectedWalletTokensData, + flow2 = settingContextUseCase.invoke(), + flow3 = marketsStateFlow, + flow4 = walletListUmFlow, + transform = { tokensData, settings, marketsData, walletList -> + val walletsUM = if (walletList.size != 1) { + WalletListUM(walletList.toPersistentList()) } else { - null + WalletListUM(persistentListOf()) } - }.distinctUntilChanged() - .transformLatest, Unit> { visibleBatchKeys -> - defaultMarketsListManager.loadCharts(visibleBatchKeys) - } - .launchIn(modelScope) + ChooseTokenUM( + walletList = walletsUM, + isBalanceHidden = settings.isBalanceHidden, + isSearching = isSearchingState, + tokensListData = tokensData, + marketsState = marketsData, + ) + }, + ) + .distinctUntilChanged() + .collect { newUM -> channel.send(newUM) } } + .flowOn(dispatchers.default) + .stateIn(modelScope, SharingStarted.Eagerly, initialValue = null) - private fun createDefaultMarketsFlow(): Flow { - val marketsTitle = TextReference.Res(R.string.feed_trending_now) - return combine( - defaultMarketsListManager.uiItems, - defaultMarketsListManager.isInInitialLoadingErrorState, - defaultMarketsListManager.totalCount, - ) { uiItems, isError, total -> - when { - isError -> SwapMarketState.LoadingError( - onRetryClicked = { defaultMarketsListManager.reload() }, - marketsTitle = marketsTitle, - shouldAssetsCount = false, - ) - uiItems.isEmpty() -> SwapMarketState.DefaultLoading - else -> SwapMarketState.Content( - items = uiItems, - loadMore = { defaultMarketsListManager.loadMore() }, - onItemClick = { item -> addToPortfolioItem(item) }, - visibleIdsChanged = { visibleDefaultMarketItemIds.value = it }, - total = total ?: uiItems.size, - marketsTitle = marketsTitle, - shouldAssetsCount = false, - ) - } - } - } - - private fun createSearchMarketsFlow(): Flow { - val marketsTitle = TextReference.Res(R.string.markets_common_title) - return combine( - flow = searchMarketsListManager.uiItems, - flow2 = searchMarketsListManager.isInInitialLoadingErrorState, - flow3 = searchMarketsListManager.isSearchNotFoundState, - flow4 = searchMarketsListManager.totalCount, - ) { uiItems, isError, isSearchNotFound, total -> - when { - isError -> SwapMarketState.LoadingError( - onRetryClicked = { searchMarketsListManager.reload(searchQueryState.value) }, - marketsTitle = marketsTitle, - shouldAssetsCount = true, - ) - isSearchNotFound -> SwapMarketState.SearchNothingFound - uiItems.isEmpty() -> SwapMarketState.SearchLoading - else -> SwapMarketState.Content( - items = uiItems, - loadMore = { searchMarketsListManager.loadMore() }, - onItemClick = { item -> addToPortfolioItem(item) }, - visibleIdsChanged = { visibleMarketItemIds.value = it }, - total = total ?: uiItems.size, - marketsTitle = marketsTitle, - shouldAssetsCount = true, - ) - } - } - } - - private fun addToPortfolioItem(item: MarketsListItemUM) { - modelScope.launch { - val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id) - ?: searchMarketsListManager.getTokenMarketById(item.id) - ?: return@launch - - val param = tokenMarket.toSerializableParam() - val hasOnlyHotWallets = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot } - - val networks = tokenMarket.networks?.filter { network -> - BlockchainUtils.isSupportedNetworkId( - blockchainId = network.networkId, - coinId = tokenMarket.id.value, - contractAddress = network.contractAddress, - excludedBlockchains = excludedBlockchains, - hotExcludedBlockchains = hotWalletExcludedBlockchains, - hasOnlyHotWallets = hasOnlyHotWallets, - ) - }?.map { network -> - TokenMarketInfo.Network( - networkId = network.networkId, - isExchangeable = false, - contractAddress = network.contractAddress, - decimalCount = network.decimalCount, - ) - }.orEmpty() - - addToPortfolioManager = addToPortfolioManagerFactory - .create( - scope = modelScope, - token = param, - analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value), - ).apply { - setTokenNetworks(networks) - } - - addToPortfolioManager?.state - ?.firstOrNull { it is AddToPortfolioManager.State.AvailableToAdd } - ?.run { bottomSheetNavigation.activate(AddToPortfolioRoute) } - }.saveIn(addToPortfolioJobHolder) + private fun onTokenItemClick(wallet: UserWallet, account: AccountStatus, currencyStatus: CryptoCurrencyStatus) { + val analyticsPayload = setOf( + ChooseTokenAnalyticsPayload.IsSearched(isSearchingState), + ) + val result = ChooseTokenResult( + account = account, + currency = currencyStatus, + wallet = wallet, + analyticsPayload = analyticsPayload, + ) + bridge.onCurrencyChosen(result) } fun onBackClicked() { bridge.onClose() } + private fun getInitialSearchBar(): SearchBarUM = SearchBarUM( + placeholderText = resourceReference(R.string.common_search), + query = "", + isActive = false, + onQueryChange = { query -> + initialState.update { prevState -> SearchBarUpdateQueryTransformer(query).transform(prevState) } + bridge.onSearchQuery(query) + }, + onActiveChange = { isActive -> + initialState.update { prevState -> SearchBarToggleTransformer(isActive).transform(prevState) } + }, + ) + + private fun getInitState() = ChooseTokenInitialUM( + screenTitle = params.settings.title, + onCloseClick = ::onBackClicked, + searchBar = getInitialSearchBar(), + ) + companion object { const val DEBOUNCE_SEARCH_DELAY = 500L } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt new file mode 100644 index 0000000000..d83753ba54 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt @@ -0,0 +1,229 @@ +package com.tangem.feature.swap.choosetoken.impl.model + +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains +import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenMarketListConfig +import com.tangem.domain.markets.toSerializableParam +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.feature.swap.models.AddToPortfolioRoute +import com.tangem.feature.swap.models.market.MarketsListBatchFlowManager +import com.tangem.feature.swap.models.market.state.SwapMarketState +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.Provider +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.CoroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch + +@Suppress("LongParameterList") +internal class MarketBlockDelegate @AssistedInject constructor( + private val marketsListBatchFlowManagerFactory: MarketsListBatchFlowManager.Factory, + private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, + private val excludedBlockchains: ExcludedBlockchains, + private val getUserWalletsUseCase: GetWalletsUseCase, + @Assisted private val modelScope: CoroutineScope, + @Assisted private val searchQueryState: StateFlow, + @Assisted private val screensSourcesName: String, +) { + + private val addToPortfolioJobHolder = JobHolder() + private val visibleMarketItemIds = MutableStateFlow>(emptyList()) + private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) + + val addToPortfolioSlot: SlotNavigation = SlotNavigation() + var addToPortfolioManager: AddToPortfolioManager? = null + + val marketsStateFlow: Flow = searchQueryState + // Switch between default and search market flows + .map { it.isEmpty() } + .distinctUntilChanged() + .flatMapLatest { isDefaultMode -> + if (isDefaultMode) { + visibleMarketItemIds.value = emptyList() + createDefaultMarketsFlow() + } else { + visibleDefaultMarketItemIds.value = emptyList() + createSearchMarketsFlow() + } + } + + private val defaultMarketsListManager by lazy { + marketsListBatchFlowManagerFactory.create( + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, + order = TokenMarketListConfig.Order.Trending, + currentSearchText = Provider { null }, + modelScope = modelScope, + ) + } + + private val searchMarketsListManager by lazy { + marketsListBatchFlowManagerFactory.create( + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, + order = TokenMarketListConfig.Order.ByRating, + currentSearchText = Provider { searchQueryState.value }, + modelScope = modelScope, + ) + } + + init { + // Reload search markets when query changes + searchQueryState + .onEach { searchQuery -> + if (searchQuery.isNotEmpty()) { + searchMarketsListManager.reload(searchQuery) + } + } + .launchIn(modelScope) + + // Initial load of default markets + defaultMarketsListManager.reload() + + visibleMarketItemIds + .mapNotNull { rawIDS -> + if (rawIDS.isNotEmpty()) { + searchMarketsListManager.getBatchKeysByItemIds(rawIDS) + } else { + null + } + } + .distinctUntilChanged() + .transformLatest, Unit> { visibleBatchKeys -> + searchMarketsListManager.loadCharts(visibleBatchKeys) + } + .launchIn(modelScope) + + visibleDefaultMarketItemIds + .mapNotNull { rawIds -> + if (rawIds.isNotEmpty()) { + defaultMarketsListManager.getBatchKeysByItemIds(rawIds) + } else { + null + } + }.distinctUntilChanged() + .transformLatest, Unit> { visibleBatchKeys -> + defaultMarketsListManager.loadCharts(visibleBatchKeys) + } + .launchIn(modelScope) + } + + private fun createDefaultMarketsFlow(): Flow { + val marketsTitle = TextReference.Res(R.string.feed_trending_now) + return combine( + defaultMarketsListManager.uiItems, + defaultMarketsListManager.isInInitialLoadingErrorState, + defaultMarketsListManager.totalCount, + ) { uiItems, isError, total -> + when { + isError -> SwapMarketState.LoadingError( + onRetryClicked = { defaultMarketsListManager.reload() }, + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + uiItems.isEmpty() -> SwapMarketState.DefaultLoading + else -> SwapMarketState.Content( + items = uiItems, + loadMore = { defaultMarketsListManager.loadMore() }, + onItemClick = { item -> addToPortfolioItem(item) }, + visibleIdsChanged = { visibleDefaultMarketItemIds.value = it }, + total = total ?: uiItems.size, + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + } + } + } + + private fun createSearchMarketsFlow(): Flow { + val marketsTitle = TextReference.Res(R.string.markets_common_title) + return combine( + flow = searchMarketsListManager.uiItems, + flow2 = searchMarketsListManager.isInInitialLoadingErrorState, + flow3 = searchMarketsListManager.isSearchNotFoundState, + flow4 = searchMarketsListManager.totalCount, + ) { uiItems, isError, isSearchNotFound, total -> + when { + isError -> SwapMarketState.LoadingError( + onRetryClicked = { searchMarketsListManager.reload(searchQueryState.value) }, + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) + isSearchNotFound -> SwapMarketState.SearchNothingFound + uiItems.isEmpty() -> SwapMarketState.SearchLoading + else -> SwapMarketState.Content( + items = uiItems, + loadMore = { searchMarketsListManager.loadMore() }, + onItemClick = { item -> addToPortfolioItem(item) }, + visibleIdsChanged = { visibleMarketItemIds.value = it }, + total = total ?: uiItems.size, + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) + } + } + } + + private fun addToPortfolioItem(item: MarketsListItemUM) { + modelScope.launch { + val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id) + ?: searchMarketsListManager.getTokenMarketById(item.id) + ?: return@launch + + val param = tokenMarket.toSerializableParam() + val hasOnlyHotWallets = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot } + + val networks = tokenMarket.networks?.filter { network -> + BlockchainUtils.isSupportedNetworkId( + blockchainId = network.networkId, + coinId = tokenMarket.id.value, + contractAddress = network.contractAddress, + excludedBlockchains = excludedBlockchains, + hotExcludedBlockchains = hotWalletExcludedBlockchains, + hasOnlyHotWallets = hasOnlyHotWallets, + ) + }?.map { network -> + TokenMarketInfo.Network( + networkId = network.networkId, + isExchangeable = false, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) + }.orEmpty() + + addToPortfolioManager = addToPortfolioManagerFactory + .create( + scope = modelScope, + token = param, + analyticsParams = AddToPortfolioManager.AnalyticsParams(source = screensSourcesName), + ).apply { + setTokenNetworks(networks) + } + + addToPortfolioManager?.state + ?.firstOrNull { it is AddToPortfolioManager.State.AvailableToAdd } + ?.run { addToPortfolioSlot.activate(AddToPortfolioRoute) } + }.saveIn(addToPortfolioJobHolder) + } + + @AssistedFactory + interface Factory { + fun create( + searchQueryState: StateFlow, + modelScope: CoroutineScope, + screensSourcesName: String, + ): MarketBlockDelegate + } +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/PortfolioListBlockDelegate.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/PortfolioListBlockDelegate.kt new file mode 100644 index 0000000000..64558e8d15 --- /dev/null +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/PortfolioListBlockDelegate.kt @@ -0,0 +1,110 @@ +package com.tangem.feature.swap.choosetoken.impl.model + +import com.tangem.common.ui.tokens.TokenConverterParams +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier +import com.tangem.domain.account.status.utils.ExpandedAccountsHolder +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.feature.swap.choosetoken.api.SettingContextUseCase +import com.tangem.feature.swap.choosetoken.impl.converter.ChooseTokenListItemConverter +import com.tangem.feature.swap.models.TokenListUMData +import com.tangem.utils.extensions.mapNotNullValues +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.* + +internal class PortfolioListBlockDelegate @AssistedInject constructor( + private val expandedAccountsHolder: ExpandedAccountsHolder, + private val settingContext: SettingContextUseCase, + private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier, + private val getWalletsUseCase: GetWalletsUseCase, + @Assisted private val modelScope: CoroutineScope, + @Assisted private val searchQueryState: StateFlow, +) : ClickIntents { + + val onTokenItemClick: Channel> = Channel() + + val portfolioList: Flow> = flow { + val allAccountsFlow: Flow> = + multiAccountStatusListSupplier.invokeAsMap() + + val allWalletsFlow: Flow> = + getWalletsUseCase.invokeAsMap() + + val expandedAccountsMapFlow = allWalletsFlow + .map { allWallets -> allWallets.values.map { wallet -> wallet.walletId } } + .distinctUntilChanged() + .flatMapLatest { walletIds -> walletIds.toExpandedAccountsMap() } + .distinctUntilChanged() + + val finalFlow = combine( + flow = settingContext.invoke(), + flow2 = allAccountsFlow, + flow3 = expandedAccountsMapFlow, + flow4 = searchQueryState, + transform = { settings, allAccounts, expandedAccountsMap, searchQuery -> + allAccounts.mapNotNullValues { (walletId, statusList) -> + val expandedAccounts = expandedAccountsMap[walletId].orEmpty() + val converterParams = if (settings.isAccountsMode) { + TokenConverterParams.Account(statusList, expandedAccounts) + } else { + TokenConverterParams.Wallet(statusList.mainAccount, statusList.mainAccount.tokenList) + } + + val um = ChooseTokenListItemConverter( + appCurrency = settings.appCurrency, + params = converterParams, + clickIntents = this@PortfolioListBlockDelegate, + searchQuery = searchQuery, + ).convert() + + um + } + }, + ) + emitAll(finalFlow) + } + .distinctUntilChanged() + .shareIn(modelScope, SharingStarted.Eagerly, replay = 1) + + private fun List.toExpandedAccountsMap(): Flow>> { + if (isEmpty()) return flowOf(emptyMap()) + val flows: List>>> = + map { walletId -> expandedAccountsHolder.expandedAccounts(walletId).map { set -> walletId to set } } + return combine(flows, { pairs -> pairs.toMap() }) + } + + override fun onTokenItemClick(account: AccountStatus, currencyStatus: CryptoCurrencyStatus) { + onTokenItemClick.trySend(account to currencyStatus) + } + + override fun onAccountExpandClick(account: Account) { + expandedAccountsHolder.expandAccount(account.accountId) + } + + override fun onAccountCollapseClick(account: Account) { + expandedAccountsHolder.collapseAccount(account.accountId) + } + + @AssistedFactory + interface Factory { + fun create(searchQueryState: StateFlow, modelScope: CoroutineScope): PortfolioListBlockDelegate + } +} + +internal interface ClickIntents { + fun onTokenItemClick(account: AccountStatus, currencyStatus: CryptoCurrencyStatus) + + fun onAccountExpandClick(account: Account) + + fun onAccountCollapseClick(account: Account) +} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt index d40b8c87ae..626a24fe4e 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt @@ -55,21 +55,15 @@ import kotlin.random.Random private const val LOAD_MORE_BUFFER = 25 @Composable -internal fun ChooseTokenScreen(state: ChooseTokenUM, modifier: Modifier = Modifier) { +internal fun ChooseTokenScreen(state: ChooseTokenFullUM, modifier: Modifier = Modifier) { Column( modifier = modifier - .background(color = TangemTheme.colors.background.tertiary) + .background(color = TangemTheme.colors.background.secondary) .fillMaxSize() - .systemBarsPadding() .imePadding(), horizontalAlignment = Alignment.CenterHorizontally, ) { - AppBar(title = state.screenTitle, onBackClick = state.onCloseClick) - SearchBar( - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), - state = state.searchBar, - colors = TangemSearchBarDefaults.secondaryTextFieldColors, - ) + AppBar(title = state.initialUM.screenTitle, onBackClick = state.initialUM.onCloseClick, Modifier) Content( state = state, @@ -79,17 +73,19 @@ internal fun ChooseTokenScreen(state: ChooseTokenUM, modifier: Modifier = Modifi } @Composable -private fun AppBar(title: TextReference, onBackClick: () -> Unit) { +private fun AppBar(title: TextReference, onBackClick: () -> Unit, modifier: Modifier = Modifier) { AppBarWithBackButton( text = title.resolveReference(), onBackClick = onBackClick, iconRes = com.tangem.common.ui.R.drawable.ic_back_24, - modifier = Modifier.height(TangemTheme.dimens.size56), + modifier = modifier + .statusBarsPadding() + .height(TangemTheme.dimens.size56), ) } @Composable -private fun Content(state: ChooseTokenUM, modifier: Modifier = Modifier) { +private fun Content(state: ChooseTokenFullUM, modifier: Modifier = Modifier) { val nestedScrollConnection = rememberHideKeyboardNestedScrollConnection() val lazyListState = rememberLazyListState() @@ -99,23 +95,34 @@ private fun Content(state: ChooseTokenUM, modifier: Modifier = Modifier) { .nestedScroll(nestedScrollConnection), horizontalAlignment = Alignment.CenterHorizontally, state = lazyListState, + contentPadding = WindowInsets.navigationBars.asPaddingValues(), ) { + item(key = "search_bar") { + SearchBar( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + state = state.initialUM.searchBar, + colors = TangemSearchBarDefaults.secondaryTextFieldColors, + ) + } + assetsTitle() - walletListItem(state.walletList) + if (state.contentUM != null) { + walletListItem(state.contentUM.walletList) - tokensListItems( - tokensListData = state.tokensListData, - isBalanceHidden = state.isBalanceHidden, - ) + tokensListItems( + tokensListData = state.contentUM.tokensListData, + isBalanceHidden = state.contentUM.isBalanceHidden, + ) - if (state.marketsState != null) { - item("markets_title_spacer") { SpacerH(height = 20.dp) } - swapMarketsListItems(state.marketsState) + if (state.contentUM.marketsState != null) { + item("markets_title_spacer") { SpacerH(height = 20.dp) } + swapMarketsListItems(state.contentUM.marketsState) + } } } - if (state.marketsState != null) { - SetupMarketScrollTracker(state.marketsState, lazyListState) + if (state.contentUM?.marketsState != null) { + SetupMarketScrollTracker(state.contentUM.marketsState, lazyListState) } } @@ -248,7 +255,7 @@ private fun LazyListScope.tokensList(items: ImmutableList, isB @Preview @Composable -private fun TokenScreenPreview(@PreviewParameter(ChooseTokenScreenPreviewProvider::class) state: ChooseTokenUM) { +private fun TokenScreenPreview(@PreviewParameter(ChooseTokenScreenPreviewProvider::class) state: ChooseTokenFullUM) { TangemThemePreview { ChooseTokenScreen( state = state, @@ -331,20 +338,24 @@ private val wallets ), ) -private class ChooseTokenScreenPreviewProvider : PreviewParameterProvider { - override val values: Sequence = sequenceOf( - ChooseTokenUM( - screenTitle = stringReference("Choose token"), - onCloseClick = {}, - walletList = WalletListUM(wallets), - searchBar = searchBar, - isBalanceHidden = false, - isAfterSearch = false, - tokensListData = TokenListUMData.AccountList( - tokensList = accounts, - accounts.size, +private class ChooseTokenScreenPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + ChooseTokenFullUM( + initialUM = ChooseTokenInitialUM( + screenTitle = stringReference("Choose token"), + onCloseClick = {}, + searchBar = searchBar, + ), + contentUM = ChooseTokenUM( + walletList = WalletListUM(wallets), + isBalanceHidden = false, + isSearching = false, + tokensListData = TokenListUMData.AccountList( + tokensList = accounts, + accounts.size, + ), + marketsState = SwapSelectTokenPreviewProvider.defaultState.marketsState, ), - marketsState = SwapSelectTokenPreviewProvider.defaultState.marketsState, ), ) } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenUM.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenUM.kt index 8b9b2e3c23..3f23f9cd85 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenUM.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenUM.kt @@ -7,17 +7,25 @@ import com.tangem.feature.swap.models.TokenListUMData import com.tangem.feature.swap.models.market.state.SwapMarketState import kotlinx.collections.immutable.ImmutableList +internal data class ChooseTokenFullUM( + val initialUM: ChooseTokenInitialUM, + val contentUM: ChooseTokenUM?, +) + internal data class ChooseTokenUM( - val screenTitle: TextReference, - val onCloseClick: () -> Unit, val walletList: WalletListUM, - val searchBar: SearchBarUM, val isBalanceHidden: Boolean, - val isAfterSearch: Boolean, + val isSearching: Boolean, val tokensListData: TokenListUMData, val marketsState: SwapMarketState?, ) +internal data class ChooseTokenInitialUM( + val screenTitle: TextReference, + val onCloseClick: () -> Unit, + val searchBar: SearchBarUM, +) + internal data class WalletListUM( val items: ImmutableList, ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt index 7303bfbb6b..d30b9a4165 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.account import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier +import com.tangem.domain.account.status.utils.ExpandedAccountsHolder import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import javax.inject.Inject diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index ebfe4ac052..35b381f033 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers +import com.tangem.common.ui.tokens.TokenConverterParams import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrency 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 1ad54a1a82..b9bbb76f34 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 @@ -7,7 +7,6 @@ import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.cardTypesResolver @@ -25,11 +24,10 @@ import com.tangem.domain.staking.model.StakingAvailability import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState -import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams +import com.tangem.common.ui.tokens.TokenConverterParams +import com.tangem.common.ui.tokens.TokenItemGrouping.toGroupedItems +import com.tangem.common.ui.tokens.TokenItemGrouping.toUngroupedItems import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.mutate -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState.OrganizeTokensButtonConfig as WalletOrganizeTokensButtonConfig @@ -92,7 +90,7 @@ internal class TokenListStateConverter( return when (params) { is TokenConverterParams.Account -> convertAccountList(params) is TokenConverterParams.Wallet -> convertTokenList( - tokenConverter = tokenStatusConverter(params.accountId), + tokenConverter = tokenStatusConverter(params.mainAccount.accountId), tokenList = params.tokenList, ) } @@ -102,11 +100,11 @@ internal class TokenListStateConverter( when (tokenList) { is TokenList.Empty -> WalletTokensListState.Empty is TokenList.GroupedByNetwork -> WalletTokensListState.ContentState.Content( - items = tokenList.toGroupedItems(tokenConverter), + items = tokenList.toGroupedItems(tokenConverter).toPersistentList(), organizeTokensButtonConfig = getOrganizeTokensButtonState(tokenList = tokenList), ) is TokenList.Ungrouped -> WalletTokensListState.ContentState.Content( - items = tokenList.toUngroupedItems(tokenConverter), + items = tokenList.toUngroupedItems(tokenConverter).toPersistentList(), organizeTokensButtonConfig = getOrganizeTokensButtonState(tokenList = tokenList), ) } @@ -167,51 +165,6 @@ internal class TokenListStateConverter( ) } - private fun TokenList.GroupedByNetwork.toGroupedItems( - tokenConverter: TokenItemStateConverter, - ): PersistentList { - return groups.fold(initial = persistentListOf()) { acc, group -> - acc.mutate { it.addGroup(tokenConverter, group) } - } - } - - private fun TokenList.Ungrouped.toUngroupedItems( - tokenConverter: TokenItemStateConverter, - ): PersistentList { - return currencies.fold(initial = persistentListOf()) { acc, token -> - acc.mutate { it.addToken(tokenConverter, token) } - } - } - - private fun MutableList.addGroup( - tokenConverter: TokenItemStateConverter, - group: NetworkGroup, - ): List { - val groupTitle = TokensListItemUM.GroupTitle( - id = group.network.hashCode(), - text = resourceReference( - id = R.string.wallet_network_group_title, - formatArgs = wrappedList(group.network.name), - ), - ) - - add(groupTitle) - group.currencies.forEach { token -> addToken(tokenConverter, token) } - - return this - } - - private fun MutableList.addToken( - tokenConverter: TokenItemStateConverter, - token: CryptoCurrencyStatus, - ): List { - val tokenItemState = tokenConverter.convert(token) - - add(TokensListItemUM.Token(tokenItemState)) - - return this - } - private fun getOrganizeTokensButtonState(tokenList: TokenList): WalletOrganizeTokensButtonConfig? { val currenciesSize = when (tokenList) { TokenList.Empty -> return null diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt index a94d6b5b78..95fa71713d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt @@ -4,7 +4,7 @@ import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.yieldSupplyKey -import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams +import com.tangem.common.ui.tokens.TokenConverterParams import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.converter.Converter import java.math.BigDecimal diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index 54b8d8deb9..6192849dc4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -16,7 +16,8 @@ import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListErrorTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams +import com.tangem.common.ui.tokens.TokenConverterParams +import com.tangem.domain.models.account.AccountStatus import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -65,7 +66,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { singleAccountTransform( maybeTokenList = maybeTokenList, appCurrency = appCurrency, - accountId = mainAccount.accountId, + mainAccount = mainAccount, yieldSupplyApyMap = yieldSupplyApyMap, stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, @@ -111,7 +112,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { private fun singleAccountTransform( maybeTokenList: Lce, appCurrency: AppCurrency, - accountId: AccountId, + mainAccount: AccountStatus.CryptoPortfolio, yieldSupplyApyMap: Map = emptyMap(), stakingAvailabilityMap: Map = emptyMap(), shouldShowMainPromo: Boolean, @@ -141,7 +142,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { ) updateContent( - params = TokenConverterParams.Wallet(accountId, tokenList), + params = TokenConverterParams.Wallet(mainAccount, tokenList), appCurrency = appCurrency, yieldSupplyApyMap = yieldSupplyApyMap, stakingAvailabilityMap = stakingAvailabilityMap, diff --git a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt index cbdb878996..2df2e0c850 100644 --- a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt +++ b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt @@ -1,9 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter import com.google.common.truth.Truth.assertThat -import com.tangem.domain.models.PortfolioId +import com.tangem.common.ui.tokens.TokenConverterParams +import com.tangem.domain.core.utils.lceError import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.account.Account.CryptoPortfolio.Companion.createMainAccount +import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network @@ -11,19 +13,25 @@ import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.yield.supply.YieldSupplyStatus -import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams import org.junit.Test import java.math.BigDecimal class YieldSupplyPromoBannerConverterTest { + private val account + get() = AccountStatus.CryptoPortfolio( + tokenList = TokenList.Empty, + priceChangeLce = Unit.lceError(), + account = createMainAccount(UserWalletId("00")) + ) + @Test fun `GIVEN promo disabled WHEN convert THEN return null`() { val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xABCDEF") val status = createLoadedStatus(token = token, amount = BigDecimal.ONE, isYieldActive = false) val tokenList = ungroupedTokenList(status) val params = TokenConverterParams.Wallet( - accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), + mainAccount = account, tokenList = tokenList, ) val converter = YieldSupplyPromoBannerConverter( @@ -41,7 +49,7 @@ class YieldSupplyPromoBannerConverterTest { val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xA1") val status = createLoadedStatus(token = token, amount = BigDecimal("2.0"), isYieldActive = false) val params = TokenConverterParams.Wallet( - accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), + mainAccount = account, tokenList = ungroupedTokenList(status), ) val converter = YieldSupplyPromoBannerConverter( @@ -59,7 +67,7 @@ class YieldSupplyPromoBannerConverterTest { val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xAA") val statusActive = createLoadedStatus(token = token, amount = BigDecimal("5"), isYieldActive = true) val params = TokenConverterParams.Wallet( - accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), + mainAccount = account, tokenList = ungroupedTokenList(statusActive), ) val converter = YieldSupplyPromoBannerConverter( @@ -87,7 +95,7 @@ class YieldSupplyPromoBannerConverterTest { ) val params = TokenConverterParams.Wallet( - accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), + mainAccount = account, tokenList = ungroupedTokenList(statusSmall, statusBig), ) val converter = YieldSupplyPromoBannerConverter( @@ -110,7 +118,7 @@ class YieldSupplyPromoBannerConverterTest { val apyMap = mapOf(mismatchedKey to BigDecimal("0.07")) val params = TokenConverterParams.Wallet( - accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), + mainAccount = account, tokenList = ungroupedTokenList(status), ) val converter = YieldSupplyPromoBannerConverter( @@ -128,7 +136,7 @@ class YieldSupplyPromoBannerConverterTest { val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xCUSTOM") val status = createCustomStatus(token = token, amount = BigDecimal("5.0"), isYieldActive = false) val params = TokenConverterParams.Wallet( - accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), + mainAccount = account, tokenList = ungroupedTokenList(status), ) val converter = YieldSupplyPromoBannerConverter( From 2e857d5e3e21ce1b52bd567a1bb901ee485257c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Apr 2026 11:26:07 +0200 Subject: [PATCH 72/75] Updated on 2026-08-14 --- .../tangem/core/ui/ds/TangemPagerIndicator.kt | 175 +++++++++++------- 1 file changed, 113 insertions(+), 62 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt index 0f4afd8e23..05fcdc6c3a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntOffset @@ -27,6 +28,7 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.extensions.conditionalCompose import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlin.math.abs @@ -40,6 +42,7 @@ private const val MIN_DISTANCE_FOR_SMALL_DOT = 3 private const val MIN_DISTANCE_FOR_HINT_DOT = 2 private val SPACING = 8.dp +private val DOT_SLOT_SIZE = 8.dp private val NORMAL_DOT_SIZE = DpSize(8.dp, 8.dp) private val HINT_DOT_SIZE = DpSize(6.dp, 6.dp) private val SMALL_DOT_SIZE = DpSize(4.dp, 4.dp) @@ -56,7 +59,6 @@ private val SMALL_DOT_SIZE = DpSize(4.dp, 4.dp) * @param modifier modifier for styling * @param colors colors of indicators(active/inactive) and overlay */ -@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun TangemPagerIndicator( pagerState: PagerState, @@ -68,62 +70,14 @@ fun TangemPagerIndicator( if (totalPages == 0) return - val density = LocalDensity.current - + val animState = rememberPagerIndicatorAnimationState(pagerState) val (targetLower, targetUpper) = getWindowBounds(totalPages, currentIndex) - var displayLower by remember { mutableIntStateOf(targetLower) } - var displayUpper by remember { mutableIntStateOf(targetUpper) } - var prevTargetLower by remember { mutableIntStateOf(targetLower) } - - val slideOffset = remember { Animatable(0f) } - var isSliding by remember { mutableStateOf(false) } - var slideDirection by remember { mutableIntStateOf(0) } - val fadeProgress = remember { Animatable(0f) } - var fadeJob by remember { mutableStateOf(null) } - LaunchedEffect(targetLower) { - if (targetLower != prevTargetLower && totalPages > MAX_VISIBLE_DOTS) { - fadeJob?.cancel() - slideOffset.stop() - fadeProgress.stop() - - val dir = if (targetLower > prevTargetLower) 1 else -1 - val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() } - val halfEdge = edgeDotSize / 2 - - isSliding = true - slideDirection = dir - fadeProgress.snapTo(0f) - - if (dir > 0) { - displayLower = prevTargetLower - displayUpper = targetUpper - slideOffset.snapTo(halfEdge) - } else { - displayLower = targetLower - displayUpper = prevTargetLower + MAX_VISIBLE_DOTS - slideOffset.snapTo(-halfEdge) - } - - prevTargetLower = targetLower - - fadeJob = launch { - fadeProgress.animateTo(1f, tween(ANIMATION_DURATION)) - } - slideOffset.animateTo( - if (dir > 0) -halfEdge else halfEdge, - tween(ANIMATION_DURATION), - ) - - displayLower = targetLower - displayUpper = targetUpper - slideOffset.snapTo(0f) - isSliding = false - slideDirection = 0 - } + animState.onBoundsChange(this, targetLower, targetUpper) } - val visibleIndices = (displayLower until displayUpper).toList() + + val visibleIndices = (animState.displayLower until animState.displayUpper).toList() Box( modifier = modifier @@ -141,20 +95,20 @@ fun TangemPagerIndicator( ) { Row( modifier = Modifier.offset { - IntOffset(slideOffset.value.roundToInt(), 0) + IntOffset(animState.slideOffset.value.roundToInt(), 0) }, horizontalArrangement = Arrangement.spacedBy(SPACING), verticalAlignment = Alignment.CenterVertically, ) { visibleIndices.forEach { index -> - val dotAlpha = when { - !isSliding -> 1f - slideDirection > 0 && index == displayLower -> 1f - fadeProgress.value - slideDirection > 0 && index == displayUpper - 1 -> fadeProgress.value - slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress.value - slideDirection < 0 && index == displayLower -> fadeProgress.value - else -> 1f - } + val dotAlpha = calculateDotAlpha( + isSliding = animState.isSliding, + slideDirection = animState.slideDirection, + index = index, + displayLower = animState.displayLower, + displayUpper = animState.displayUpper, + fadeProgress = animState.fadeProgress.value, + ) key(index) { Dot( @@ -171,6 +125,103 @@ fun TangemPagerIndicator( } } +@Composable +private fun rememberPagerIndicatorAnimationState(pagerState: PagerState): PagerIndicatorAnimationState { + val density = LocalDensity.current + return remember(pagerState.pageCount, density) { + PagerIndicatorAnimationState(pagerState.pageCount, pagerState.currentPage, density) + } +} + +@Suppress("LongParameterList") +private fun calculateDotAlpha( + isSliding: Boolean, + slideDirection: Int, + index: Int, + displayLower: Int, + displayUpper: Int, + fadeProgress: Float, +): Float { + return when { + !isSliding -> 1f + slideDirection > 0 && index == displayLower -> 1f - fadeProgress + slideDirection > 0 && index == displayUpper - 1 -> fadeProgress + slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress + slideDirection < 0 && index == displayLower -> fadeProgress + else -> 1f + } +} + +@Stable +private class PagerIndicatorAnimationState( + private val totalPages: Int, + initialCurrentPage: Int, + private val density: Density, +) { + var displayLower by mutableIntStateOf(0) + private set + var displayUpper by mutableIntStateOf(0) + private set + + val slideOffset = Animatable(0f) + var isSliding by mutableStateOf(false) + private set + var slideDirection by mutableIntStateOf(0) + private set + val fadeProgress = Animatable(0f) + private var fadeJob: Job? = null + private var prevTargetLower: Int = 0 + + init { + val (lower, upper) = getWindowBounds(totalPages, initialCurrentPage) + displayLower = lower + displayUpper = upper + prevTargetLower = lower + } + + suspend fun onBoundsChange(scope: CoroutineScope, targetLower: Int, targetUpper: Int) { + if (targetLower == prevTargetLower || totalPages <= MAX_VISIBLE_DOTS) { + return + } + + fadeJob?.cancel() + slideOffset.stop() + fadeProgress.stop() + + val dir = if (targetLower > prevTargetLower) 1 else -1 + val dotSlot = with(density) { (DOT_SLOT_SIZE + SPACING).toPx() } + + isSliding = true + slideDirection = dir + fadeProgress.snapTo(0f) + + if (dir > 0) { + displayUpper = targetUpper + slideOffset.snapTo(0f) + } else { + displayLower = targetLower + displayUpper = prevTargetLower + MAX_VISIBLE_DOTS + slideOffset.snapTo(-dotSlot) + } + + prevTargetLower = targetLower + + fadeJob = scope.launch { + fadeProgress.animateTo(1f, tween(ANIMATION_DURATION)) + } + slideOffset.animateTo( + if (dir > 0) -dotSlot else 0f, + tween(ANIMATION_DURATION), + ) + + displayLower = targetLower + displayUpper = targetUpper + slideOffset.snapTo(0f) + isSliding = false + slideDirection = 0 + } +} + @Suppress("MagicNumber") @Composable private fun getSize(pageCount: Int): Dp { From 45fa1204114ffe411f694dc475cbddf2e5fe667d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Apr 2026 11:40:31 +0200 Subject: [PATCH 73/75] Updated on 2026-08-14 --- .../common/ui/markets/MarketListItemV2.kt | 10 ++- .../markets/MarketsListItemPriceAnnotated.kt | 22 +++++ .../common/ui/markets/MarketsListItemV1.kt | 8 ++ .../ui/markets/models/MarketsListItemUM.kt | 3 + .../MarketChartListItemPreviewDataProvider.kt | 37 ++++++-- .../tangem/common/ui/tokens/TokenPriceText.kt | 50 +++++------ .../response/TokenMarketInfoResponse.kt | 2 + .../format/bigdecimal/BigDecimalFiatFormat.kt | 34 ++++++++ .../tangem/core/ui/res/TangemTypography2.kt | 12 +++ .../main/res/drawable/ic_yield_mode_16.xml | 9 ++ .../converters/TokenMarketInfoConverter.kt | 1 + .../converter/SearchHistoryConverter.kt | 25 ++++++ .../tangem/data/search/model/SearchHistory.kt | 12 +++ .../tangem/domain/markets/TokenMarketInfo.kt | 1 + .../domain/search/model/RecentSearchToken.kt | 11 +++ .../search/model/TokenPriceChangeDirection.kt | 7 ++ ...nTokenWithCurrencyToListItemUMConverter.kt | 3 +- .../converter/MarketsTokenItemConverter.kt | 6 ++ .../details/MarketsTokenDetailsModel.kt | 10 ++- .../details/converter/MetricsConverter.kt | 17 +++- .../market/details/formatter/Formatters.kt | 18 +++- .../details/state/QuotesStateUpdater.kt | 1 + .../converter/RelatedTokenConverter.kt | 6 ++ .../features/feed/model/search/SearchModel.kt | 84 ++++++++++++++----- ...sListItemUMToRecentSearchTokenConverter.kt | 41 +++++++++ .../MarketsListItemUMWithAppCurrency.kt | 9 ++ ...SearchTokenToMarketsListItemUMConverter.kt | 55 ++++++++++++ .../RecentSearchTokenWithAppCurrency.kt | 9 ++ .../UpdateRecentTokenChartTransformer.kt | 30 +++++++ .../features/feed/ui/earn/EarnContent.kt | 4 +- .../feed/ui/earn/components/EarnListItem.kt | 27 ++++-- .../feed/ui/earn/components/MostlyUsedCard.kt | 7 +- .../features/feed/ui/earn/state/EarnListUM.kt | 4 +- .../preview/FeedListPreviewDataProvider.kt | 11 ++- .../detailed/MarketsTokenDetailsContent.kt | 58 +++++++++++++ .../detailed/components/MetricsCards.kt | 41 +++++++-- .../preview/MarketsTokenDetailsPreview.kt | 2 + .../detailed/state/MarketsTokenDetailsUM.kt | 1 + .../ui/market/detailed/state/MetricsUM.kt | 1 + .../features/feed/ui/search/SearchContent.kt | 10 ++- .../ui/search/preview/SearchContentPreview.kt | 9 +- .../feed/ui/search/state/SearchCallbacks.kt | 4 +- features/markets/impl/build.gradle.kts | 1 + .../block/impl/model/TokenMarketBlockModel.kt | 13 ++- .../block/impl/ui/TokenMarketBlockLegacy.kt | 12 +++ .../block/impl/ui/state/TokenMarketBlockUM.kt | 4 + .../SwapMarketsTokenItemConverter.kt | 6 ++ .../SwapMarketsTokenItemConverter.kt | 6 ++ .../preview/SwapSelectTokenPreviewProvider.kt | 7 +- 49 files changed, 669 insertions(+), 92 deletions(-) create mode 100644 common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketsListItemPriceAnnotated.kt create mode 100644 core/ui/src/main/res/drawable/ic_yield_mode_16.xml create mode 100644 domain/search/src/main/java/com/tangem/domain/search/model/TokenPriceChangeDirection.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/MarketsListItemUMToRecentSearchTokenConverter.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/MarketsListItemUMWithAppCurrency.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/RecentSearchTokenToMarketsListItemUMConverter.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/RecentSearchTokenWithAppCurrency.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateRecentTokenChartTransformer.kt diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItemV2.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItemV2.kt index df7eea3655..9f5a2c1e63 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItemV2.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItemV2.kt @@ -39,6 +39,7 @@ import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds.row.TangemRowContainer import com.tangem.core.ui.ds.row.TangemRowLayoutId import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.LocalIsInDarkTheme import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme @@ -47,6 +48,7 @@ import com.tangem.core.ui.test.MarketsTestTags import com.tangem.core.ui.test.TokenElementsTestTags import com.tangem.core.ui.windowsize.WindowSizeType import com.tangem.utils.StringsSigns.MINUS +import java.math.BigDecimal import kotlin.random.Random @Composable @@ -88,6 +90,8 @@ fun MarketListItemContentV2(model: MarketsListItemUM, modifier: Modifier = Modif .testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT), price = model.price.text, priceChangeType = model.price.changeType, + priceAnnotated = model.price.annotated, + priceValue = model.price.fiatPrice, ) TokenSubtitle( @@ -127,7 +131,7 @@ private fun TokenTitle(name: String, currencySymbol: String, modifier: Modifier .alignByBaseline(), text = name, color = TangemTheme.colors2.text.neutral.primary, - style = TangemTheme.typography2.bodySemibold16, + style = TangemTheme.typography2.bodyMedium16, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -323,12 +327,16 @@ private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::cl price = MarketsListItemUM.Price( text = "0.${prices[0].first}023 $", changeType = prices[0].second, + fiatPrice = BigDecimal(123123), + annotated = stringReference("0.${prices[0].first}023 $"), ), ) state2 = state2.copy( price = MarketsListItemUM.Price( text = "0.${prices[1].first}023 $", changeType = prices[1].second, + fiatPrice = BigDecimal(123123), + annotated = stringReference("0.${prices[0].first}023 $"), ), ) }, diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketsListItemPriceAnnotated.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketsListItemPriceAnnotated.kt new file mode 100644 index 0000000000..a972e8801d --- /dev/null +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketsListItemPriceAnnotated.kt @@ -0,0 +1,22 @@ +package com.tangem.common.ui.markets + +import androidx.compose.ui.text.SpanStyle +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.formatStyled +import com.tangem.core.ui.format.bigdecimal.price +import com.tangem.core.ui.res.TangemTheme +import java.math.BigDecimal + +/** + * Fiat amount for markets list rows: integer part in primary style, fractional part (from locale separator) in secondary. + */ +fun BigDecimal.toMarketsListItemPriceAnnotated(appCurrencyCode: String, appCurrencySymbol: String): TextReference { + return formatStyled { + fiat( + fiatCurrencyCode = appCurrencyCode, + fiatCurrencySymbol = appCurrencySymbol, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ).price() + } +} \ No newline at end of file diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketsListItemV1.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketsListItemV1.kt index bfa927d620..6d051b3cff 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketsListItemV1.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketsListItemV1.kt @@ -29,12 +29,14 @@ import com.tangem.core.ui.components.marketprice.PriceChangeInPercent import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.MarketsTestTags import com.tangem.core.ui.windowsize.WindowSizeType import com.tangem.utils.StringsSigns.MINUS +import java.math.BigDecimal import kotlin.random.Random @Composable @@ -85,6 +87,8 @@ private fun MarketsListItemContentV1(model: MarketsListItemUM, modifier: Modifie modifier = Modifier.alignByBaseline(), price = model.price.text, priceChangeType = model.price.changeType, + priceAnnotated = model.price.annotated, + priceValue = model.price.fiatPrice, ) } @@ -306,12 +310,16 @@ private fun Preview(@PreviewParameter(MarketChartListItemPreviewDataProvider::cl price = MarketsListItemUM.Price( text = "0.${prices[0].first}023 $", changeType = prices[0].second, + fiatPrice = BigDecimal(123123), + annotated = stringReference("0.${prices[0].first}023 $"), ), ) state2 = state2.copy( price = MarketsListItemUM.Price( text = "0.${prices[1].first}023 $", changeType = prices[1].second, + fiatPrice = BigDecimal(123123), + annotated = stringReference("0.${prices[0].first}023 $"), ), ) }, diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/models/MarketsListItemUM.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/models/MarketsListItemUM.kt index 7659617208..9136b31345 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/models/MarketsListItemUM.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/models/MarketsListItemUM.kt @@ -6,6 +6,7 @@ import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrency +import java.math.BigDecimal @Immutable data class MarketsListItemUM( @@ -40,7 +41,9 @@ data class MarketsListItemUM( @Immutable data class Price( val text: String, + val annotated: TextReference, val changeType: PriceChangeType? = null, + val fiatPrice: BigDecimal, ) @Suppress("NullableToStringCall") diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/preview/MarketChartListItemPreviewDataProvider.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/preview/MarketChartListItemPreviewDataProvider.kt index 29cd1685ff..0956d1a6a0 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/preview/MarketChartListItemPreviewDataProvider.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/preview/MarketChartListItemPreviewDataProvider.kt @@ -7,6 +7,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal @Suppress("MagicNumber") class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvider( @@ -18,7 +19,11 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide iconUrl = "", ratingPosition = "1", marketCap = "$6.233 B", - price = MarketsListItemUM.Price(text = "31 285.72$"), + price = MarketsListItemUM.Price( + text = "31 285.72$", + fiatPrice = BigDecimal(123123), + annotated = stringReference("31 285.72$"), + ), trendPercentText = "12.43%", trendType = PriceChangeType.UP, chartData = MarketChartRawData( @@ -35,7 +40,11 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide iconUrl = null, ratingPosition = "2", marketCap = "$6.233 B", - price = MarketsListItemUM.Price(text = "31 285.72$"), + price = MarketsListItemUM.Price( + text = "31 285.72$", + fiatPrice = BigDecimal(123123), + annotated = stringReference("31 285.72$"), + ), trendPercentText = "12.43%", trendType = PriceChangeType.NEUTRAL, chartData = null, @@ -50,7 +59,11 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide iconUrl = null, ratingPosition = "10", marketCap = "$6.23348172384781234 B", - price = MarketsListItemUM.Price(text = "31 285.72$"), + price = MarketsListItemUM.Price( + text = "31 285.72$", + fiatPrice = BigDecimal(123123), + annotated = stringReference("31 285.72$"), + ), trendPercentText = "12.43%", trendType = PriceChangeType.DOWN, chartData = MarketChartRawData( @@ -67,7 +80,11 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide iconUrl = null, ratingPosition = "10", marketCap = null, - price = MarketsListItemUM.Price(text = "31 285.72$"), + price = MarketsListItemUM.Price( + text = "31 285.72$", + fiatPrice = BigDecimal(123123), + annotated = stringReference("31 285.72$"), + ), trendPercentText = "12.43%", trendType = PriceChangeType.UP, chartData = MarketChartRawData( @@ -84,7 +101,11 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide iconUrl = null, ratingPosition = null, marketCap = "$6.233 B", - price = MarketsListItemUM.Price(text = "31 285.72$"), + price = MarketsListItemUM.Price( + text = "31 285.72$", + fiatPrice = BigDecimal(123123), + annotated = stringReference("31 285.72$"), + ), trendPercentText = "12.43%", trendType = PriceChangeType.UP, chartData = MarketChartRawData( @@ -101,7 +122,11 @@ class MarketChartListItemPreviewDataProvider : CollectionPreviewParameterProvide iconUrl = null, ratingPosition = null, marketCap = null, - price = MarketsListItemUM.Price(text = "31 285.72$"), + price = MarketsListItemUM.Price( + text = "31 285.72$", + fiatPrice = BigDecimal(123123), + annotated = stringReference("31 285.72$"), + ), trendPercentText = "12.43%", trendType = PriceChangeType.UP, chartData = MarketChartRawData( diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenPriceText.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenPriceText.kt index df80ddeae9..7ab071bfbd 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenPriceText.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenPriceText.kt @@ -6,25 +6,33 @@ import androidx.compose.animation.core.tween import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.withStyle import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveAnnotatedReference import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme +import java.math.BigDecimal /** * Text view for token price. * - * @param price Price of the token. + * @param price Plain price string (used by V1 and as fallback for V2 when [priceAnnotated] is null). + * @param priceAnnotated Optional styled price (locale-aware decimal separator + secondary fractional color). * @param priceChangeType Type of the price change. */ @Composable -fun TokenPriceText(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) { +fun TokenPriceText( + priceValue: BigDecimal, + price: String, + priceAnnotated: TextReference, + modifier: Modifier = Modifier, + priceChangeType: PriceChangeType? = null, +) { if (LocalRedesignEnabled.current) { TokenPriceTextV2( - price = price, + priceValue = priceValue, + priceAnnotated = priceAnnotated, modifier = modifier, priceChangeType = priceChangeType, ) @@ -76,16 +84,20 @@ private fun TokenPriceTextV1(price: String, modifier: Modifier = Modifier, price } @Composable -private fun TokenPriceTextV2(price: String, modifier: Modifier = Modifier, priceChangeType: PriceChangeType? = null) { +private fun TokenPriceTextV2( + priceValue: BigDecimal, + priceAnnotated: TextReference, + modifier: Modifier = Modifier, + priceChangeType: PriceChangeType? = null, +) { val growColor = TangemTheme.colors2.text.status.accent val fallColor = TangemTheme.colors2.text.status.warning val generalColor = TangemTheme.colors2.text.neutral.primary - val decimalColor = TangemTheme.colors2.text.neutral.secondary val color = remember(generalColor) { Animatable(generalColor) } var isAnimationSkipped by remember { mutableStateOf(false) } - LaunchedEffect(price) { + LaunchedEffect(priceValue) { if (!isAnimationSkipped) { isAnimationSkipped = true return@LaunchedEffect @@ -103,27 +115,9 @@ private fun TokenPriceTextV2(price: String, modifier: Modifier = Modifier, price } } - val annotatedText = remember(price) { - buildAnnotatedString { - val dotIndex = price.indexOf(".") - - if (dotIndex == -1) { - append(price) - } else { - append(price.take(dotIndex)) - - withStyle( - style = SpanStyle(color = decimalColor), - ) { - append(price.substring(dotIndex)) - } - } - } - } - Text( modifier = modifier, - text = annotatedText, + text = priceAnnotated.resolveAnnotatedReference(), color = color.value, maxLines = 1, style = TangemTheme.typography2.bodySemibold16, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt index 8472387cb9..21c7d87c86 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketInfoResponse.kt @@ -116,6 +116,8 @@ data class TokenMarketInfoResponse( val maxSupply: BigDecimal?, @Json(name = "fully_diluted_valuation") val fullyDilutedValuation: BigDecimal?, + @Json(name = "fully_diluted_valuation_change_24h") + val fullyDilutedValuationChange24H: BigDecimal?, ) @JsonClass(generateAdapter = true) diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt index a1f6549b2b..2c29aedd8b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt @@ -172,6 +172,40 @@ fun BigDecimalFiatFormat.price(): BigDecimalFormat = BigDecimalFormat { value -> .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) } +/** + * Formats fiat price with precision calculated based on the value. + * Styled version — fractional part uses [spanStyleReference]. + * @see getFiatPriceAmountWithScale + */ +fun BigDecimalFiatFormatStyled.price() = price(spanStyleReference) + +private fun BigDecimalFiatFormatStyled.price(spanStyleReference: SpanStyleReference) = BigDecimalFormatStyled { 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 + } + + val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator + val formattedAmount = formatter.format(priceAmount) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + + val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + + val wholePart = formattedAmount.take(separatorIndex) + val fractionalPart = formattedAmount.drop(separatorIndex) + + combinedReference( + stringReference(wholePart), + styledStringReference(fractionalPart, spanStyleReference), + ) +} + /** * Formats fiat amount with an exact number of fractional digits. */ diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography2.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography2.kt index 8404f3a578..38675546fd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTypography2.kt @@ -198,6 +198,18 @@ class TangemTypography2 internal constructor( ), ) + val bodyMedium16: TextStyle = TextStyle( + fontFamily = fontFamily, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + letterSpacing = TextUnit(value = -0.32f, type = TextUnitType.Sp), + lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp), + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), + ) + val bodyRegular15: TextStyle = TextStyle( fontFamily = fontFamily, fontSize = 15.sp, diff --git a/core/ui/src/main/res/drawable/ic_yield_mode_16.xml b/core/ui/src/main/res/drawable/ic_yield_mode_16.xml new file mode 100644 index 0000000000..91bfaf07df --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_yield_mode_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt index 8a6cee1f3f..bbcfcb10ab 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketInfoConverter.kt @@ -111,6 +111,7 @@ internal class TokenMarketInfoConverter( volume24h = volume24h, maxSupply = maxSupply, fullyDilutedValuation = fullyDilutedValuation, + fullyDilutedValuationChange24 = fullyDilutedValuationChange24H, ) } diff --git a/data/search/src/main/java/com/tangem/data/search/converter/SearchHistoryConverter.kt b/data/search/src/main/java/com/tangem/data/search/converter/SearchHistoryConverter.kt index 60bedf12bd..68ccb8eec8 100644 --- a/data/search/src/main/java/com/tangem/data/search/converter/SearchHistoryConverter.kt +++ b/data/search/src/main/java/com/tangem/data/search/converter/SearchHistoryConverter.kt @@ -5,7 +5,9 @@ import com.tangem.data.search.model.TextHintDTO import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.search.model.RecentSearchToken import com.tangem.domain.search.model.SearchTextHint +import com.tangem.domain.search.model.TokenPriceChangeDirection import com.tangem.utils.converter.Converter +import java.math.BigDecimal internal class TextHintDTOToSearchTextHintConverter : Converter { override fun convert(value: TextHintDTO): SearchTextHint { @@ -24,6 +26,15 @@ internal class RecentTokenDTOToRecentSearchTokenConverter : Converter TextReference.Res(R.string.common_staking) EarnType.YIELD -> TextReference.Res(R.string.common_yield_mode) }, + earnType = value.earnToken.type, onItemClick = { onItemClick(value) }, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt index 3dea584bad..dda2c04ee8 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt @@ -5,6 +5,7 @@ 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.common.ui.markets.models.MarketsListItemUM +import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList @@ -110,7 +111,12 @@ internal class MarketsTokenItemConverter( return MarketsListItemUM.Price( text = priceText, + annotated = tokenQuotesShort.currentPrice.toMarketsListItemPriceAnnotated( + appCurrencyCode = appCurrency.code, + appCurrencySymbol = appCurrency.symbol, + ), changeType = changeType, + fiatPrice = tokenQuotesShort.currentPrice, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt index b92a688326..85cb23c471 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt @@ -230,6 +230,9 @@ internal class MarketsTokenDetailsModel @Inject constructor( fiatCurrencySymbol = currentAppCurrency.value.symbol, ).price() }, + priceAnnotated = params.token.tokenQuotes.currentPrice.toMarketsTokenDetailsPriceAnnotated( + currentAppCurrency.value, + ), dateTimeText = resourceReference(R.string.common_today), priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() }, priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(), @@ -505,6 +508,9 @@ internal class MarketsTokenDetailsModel @Inject constructor( fiatCurrencyCode = currentAppCurrency.value.code, ).price() }, + priceAnnotated = newInfo.quotes.currentPrice.toMarketsTokenDetailsPriceAnnotated( + currentAppCurrency.value, + ), priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval( interval = marketsTokenDetailsUM.selectedInterval, ), @@ -604,7 +610,8 @@ internal class MarketsTokenDetailsModel @Inject constructor( ) } ?: getDefaultDateTimeString(currentState.selectedInterval) - val priceText = (price ?: currentQuotes.value.currentPrice).format { + val amountForPrice = price ?: currentQuotes.value.currentPrice + val priceText = amountForPrice.format { fiat( fiatCurrencySymbol = currentAppCurrency.value.symbol, fiatCurrencyCode = currentAppCurrency.value.code, @@ -625,6 +632,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( isMarkerSet = markerTimestamp != null, dateTimeText = dateTimeText, priceText = priceText, + priceAnnotated = amountForPrice.toMarketsTokenDetailsPriceAnnotated(currentAppCurrency.value), priceChangePercentText = percentText, priceChangeType = percent.percentChangeType(), ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/converter/MetricsConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/converter/MetricsConverter.kt index 3422433ee1..ffd70f6371 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/converter/MetricsConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/converter/MetricsConverter.kt @@ -1,10 +1,7 @@ package com.tangem.features.feed.model.market.details.converter import androidx.compose.runtime.Stable -import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.compact import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat @@ -188,6 +185,7 @@ internal class MetricsConverter( formatArgs = wrappedList(formatted), ) }, + fullyDilutedValuationChange24 = fullyDilutedValuationChange24?.convertChange(), onInfoClick = { onInfoClick( InfoBottomSheetContent( @@ -280,6 +278,17 @@ internal class MetricsConverter( } } + private fun BigDecimal?.convertChange(): TextReference? { + if (this == null) return null + val amount = this.abs().formatAmount() + val value = when { + this > BigDecimal.ZERO -> StringsSigns.PLUS + amount + this < BigDecimal.ZERO -> StringsSigns.MINUS + amount + else -> amount + } + return value?.let(::stringReference) + } + @Suppress("MagicNumber") private fun getLiquidity(volume24h: BigDecimal?, marketCap: BigDecimal?): Float? { if (volume24h == null || marketCap == null || marketCap == BigDecimal.ZERO) return null diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/formatter/Formatters.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/formatter/Formatters.kt index 218580167e..409fcf4e92 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/formatter/Formatters.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/formatter/Formatters.kt @@ -1,10 +1,12 @@ package com.tangem.features.feed.model.market.details.formatter +import androidx.compose.ui.text.SpanStyle import com.tangem.common.ui.charts.state.MarketChartLook import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.getFiatPriceAmountWithScale -import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.format.bigdecimal.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.markets.TokenQuotes import java.math.BigDecimal @@ -73,4 +75,14 @@ internal fun PriceChangeType.toChartType(): MarketChartLook.Type { PriceChangeType.DOWN -> MarketChartLook.Type.Falling PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral } +} + +internal fun BigDecimal.toMarketsTokenDetailsPriceAnnotated(appCurrency: AppCurrency): TextReference { + return formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.tertiary) }, + ).price() + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/state/QuotesStateUpdater.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/state/QuotesStateUpdater.kt index 8ea1c11938..c646386959 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/state/QuotesStateUpdater.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/state/QuotesStateUpdater.kt @@ -66,6 +66,7 @@ internal class QuotesStateUpdater( fiatCurrencyCode = currentAppCurrency().code, ).price() }, + priceAnnotated = newQuotes.currentPrice.toMarketsTokenDetailsPriceAnnotated(currentAppCurrency()), priceChangePercentText = newQuotes.getFormattedPercentByInterval( interval = stateToUpdate.selectedInterval, ), diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/converter/RelatedTokenConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/converter/RelatedTokenConverter.kt index 27c51c52e0..ef61cb6cc5 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/converter/RelatedTokenConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/converter/RelatedTokenConverter.kt @@ -6,6 +6,7 @@ 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.common.ui.markets.models.MarketsListItemUM +import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.format.bigdecimal.* import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost @@ -68,7 +69,12 @@ internal class RelatedTokenConverter(private val appCurrency: AppCurrency) : return MarketsListItemUM.Price( text = priceText, + annotated = tokenInfo.quotes.currentPrice.toMarketsListItemPriceAnnotated( + appCurrencyCode = appCurrency.code, + appCurrencySymbol = appCurrency.symbol, + ), changeType = null, + fiatPrice = tokenInfo.quotes.currentPrice, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt index a21b032562..2273f437bb 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt @@ -5,19 +5,27 @@ import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.common.ui.charts.state.MarketChartData +import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter +import com.tangem.common.ui.charts.state.sorted import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase +import com.tangem.domain.markets.GetTokenPriceChartUseCase +import com.tangem.domain.markets.PriceChangeInterval import com.tangem.domain.models.account.AccountName -import com.tangem.domain.search.model.RecentSearchToken import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase import com.tangem.domain.search.usecase.GetSearchResultsUseCase +import com.tangem.domain.search.usecase.SaveRecentSearchTokenUseCase import com.tangem.domain.search.usecase.SaveSearchQueryUseCase import com.tangem.features.feed.components.search.DefaultSearchComponent import com.tangem.features.feed.model.market.list.state.MarketsListUM import com.tangem.features.feed.model.market.list.state.SortByTypeUM import com.tangem.features.feed.model.market.list.statemanager.MarketsListBatchFlowManager +import com.tangem.features.feed.model.search.converter.MarketsListItemUMToRecentSearchTokenConverter +import com.tangem.features.feed.model.search.converter.MarketsListItemUMWithAppCurrency +import com.tangem.features.feed.model.search.converter.RecentSearchTokenToMarketsListItemUMConverter +import com.tangem.features.feed.model.search.converter.RecentSearchTokenWithAppCurrency import com.tangem.features.feed.model.search.state.SearchStateController import com.tangem.features.feed.model.search.state.transformers.* import com.tangem.features.feed.ui.search.state.* @@ -28,6 +36,9 @@ import com.tangem.utils.coroutines.saveIn import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -45,7 +56,9 @@ internal class SearchModel @Inject constructor( getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getSearchResultsUseCase: GetSearchResultsUseCase, private val saveSearchQueryUseCase: SaveSearchQueryUseCase, + private val saveRecentSearchTokenUseCase: SaveRecentSearchTokenUseCase, private val clearSearchHistoryUseCase: ClearSearchHistoryUseCase, + private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase, private val stateController: SearchStateController, ) : Model() { @@ -64,6 +77,18 @@ internal class SearchModel @Inject constructor( initialValue = AppCurrency.Default, ) + private val marketsListItemToRecentSearchTokenConverter by lazy { + MarketsListItemUMToRecentSearchTokenConverter() + } + + private val recentSearchTokenToMarketsListItemConverter by lazy { + RecentSearchTokenToMarketsListItemUMConverter() + } + + private val priceAndTimePointValuesConverter by lazy { + PriceAndTimePointValuesConverter(shouldFormatAxis = false) + } + private val searchMarketsListManager by lazy { MarketsListBatchFlowManager( getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, @@ -108,8 +133,15 @@ internal class SearchModel @Inject constructor( stateController.update(UpdateSearchBarQueryTransformer(text)) } - fun onResultMarketTokenClick() { + fun onResultMarketTokenClick(item: MarketsListItemUM) { modelScope.launch(dispatchers.default) { + val appCurrency = currentAppCurrency.value + val input = MarketsListItemUMWithAppCurrency( + item = item, + appCurrencyCode = appCurrency.code, + appCurrencySymbol = appCurrency.symbol, + ) + saveRecentSearchTokenUseCase(marketsListItemToRecentSearchTokenConverter.convert(input)) saveSearchQueryUseCase(stateController.value.searchBar.query) } } @@ -263,31 +295,43 @@ internal class SearchModel @Inject constructor( TextHintItemUM(text = hint.text) }.toImmutableList() + val appCurrency = currentAppCurrency.value val recentTokens = searchResult.recentTokens.map { token -> - token.toMarketsListItemUM() + recentSearchTokenToMarketsListItemConverter.convert( + RecentSearchTokenWithAppCurrency(token = token, appCurrency = appCurrency), + ) }.toImmutableList() stateController.update(UpdateHistoryTransformer(textHints, recentTokens)) + + loadRecentTokenCharts(recentTokens, appCurrency) } }.saveIn(searchResultsJob) } - private fun RecentSearchToken.toMarketsListItemUM(): MarketsListItemUM { - return MarketsListItemUM( - id = id, - name = name, - currencySymbol = symbol, - iconUrl = imageUrl, - ratingPosition = null, - marketCap = null, - price = MarketsListItemUM.Price(text = ""), - trendPercentText = "", - trendType = PriceChangeType.NEUTRAL, - chartData = null, - isUnder100kMarketCap = false, - stakingRate = null, - updateTimestamp = timestamp, - ) + private suspend fun loadRecentTokenCharts(tokens: ImmutableList, appCurrency: AppCurrency) { + coroutineScope { + tokens.map { token -> + async(dispatchers.io) { + val chart = getTokenPriceChartUseCase( + appCurrency = appCurrency, + interval = PriceChangeInterval.H24, + tokenId = token.id, + tokenSymbol = token.currencySymbol, + preview = true, + ).getOrElse { return@async } + + val chartData = priceAndTimePointValuesConverter.convert( + MarketChartData.Data( + y = chart.priceY.toImmutableList(), + x = chart.timeStamps.map { it.toBigDecimal() }.toImmutableList(), + ).sorted(), + ) + + stateController.update(UpdateRecentTokenChartTransformer(token.id, chartData)) + } + }.awaitAll() + } } private fun AccountName.toDisplayString(): String { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/MarketsListItemUMToRecentSearchTokenConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/MarketsListItemUMToRecentSearchTokenConverter.kt new file mode 100644 index 0000000000..cd17f16458 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/MarketsListItemUMToRecentSearchTokenConverter.kt @@ -0,0 +1,41 @@ +package com.tangem.features.feed.model.search.converter + +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.search.model.RecentSearchToken +import com.tangem.domain.search.model.TokenPriceChangeDirection +import com.tangem.utils.converter.Converter + +internal class MarketsListItemUMToRecentSearchTokenConverter : + Converter { + + override fun convert(value: MarketsListItemUMWithAppCurrency): RecentSearchToken { + val item = value.item + val direction = when (item.trendType) { + PriceChangeType.UP -> TokenPriceChangeDirection.UP + PriceChangeType.DOWN -> TokenPriceChangeDirection.DOWN + PriceChangeType.NEUTRAL -> TokenPriceChangeDirection.NEUTRAL + } + val stakingText = when (val rate = item.stakingRate) { + is TextReference.Str -> rate.value + is TextReference.StyledStr -> rate.value + else -> null + } + return RecentSearchToken( + id = item.id, + name = item.name, + symbol = item.currencySymbol, + imageUrl = item.iconUrl, + timestamp = item.updateTimestamp ?: System.currentTimeMillis(), + appCurrencyCode = value.appCurrencyCode, + appCurrencySymbol = value.appCurrencySymbol, + price = item.price.fiatPrice, + priceChangePercent = item.trendPercentText, + priceChangeDirection = direction, + marketCap = item.marketCap, + ratingPosition = item.ratingPosition, + isUnder100kMarketCap = item.isUnder100kMarketCap, + stakingRateText = stakingText, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/MarketsListItemUMWithAppCurrency.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/MarketsListItemUMWithAppCurrency.kt new file mode 100644 index 0000000000..e2c566a31b --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/MarketsListItemUMWithAppCurrency.kt @@ -0,0 +1,9 @@ +package com.tangem.features.feed.model.search.converter + +import com.tangem.common.ui.markets.models.MarketsListItemUM + +internal data class MarketsListItemUMWithAppCurrency( + val item: MarketsListItemUM, + val appCurrencyCode: String, + val appCurrencySymbol: String, +) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/RecentSearchTokenToMarketsListItemUMConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/RecentSearchTokenToMarketsListItemUMConverter.kt new file mode 100644 index 0000000000..840613cb88 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/RecentSearchTokenToMarketsListItemUMConverter.kt @@ -0,0 +1,55 @@ +package com.tangem.features.feed.model.search.converter + +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.price +import com.tangem.domain.search.model.TokenPriceChangeDirection +import com.tangem.utils.converter.Converter + +internal class RecentSearchTokenToMarketsListItemUMConverter : + Converter { + + override fun convert(value: RecentSearchTokenWithAppCurrency): MarketsListItemUM { + val token = value.token + val currencyCode = token.appCurrencyCode.ifEmpty { value.appCurrency.code } + val currencySymbol = token.appCurrencySymbol.ifEmpty { value.appCurrency.symbol } + val trendType = when (token.priceChangeDirection) { + TokenPriceChangeDirection.UP -> PriceChangeType.UP + TokenPriceChangeDirection.DOWN -> PriceChangeType.DOWN + TokenPriceChangeDirection.NEUTRAL -> PriceChangeType.NEUTRAL + } + val priceText = token.price.format { + fiat( + fiatCurrencyCode = currencyCode, + fiatCurrencySymbol = currencySymbol, + ).price() + } + return MarketsListItemUM( + id = token.id, + name = token.name, + currencySymbol = token.symbol, + iconUrl = token.imageUrl, + ratingPosition = token.ratingPosition, + marketCap = token.marketCap, + price = MarketsListItemUM.Price( + text = priceText, + annotated = token.price.toMarketsListItemPriceAnnotated( + appCurrencyCode = currencyCode, + appCurrencySymbol = currencySymbol, + ), + changeType = null, + fiatPrice = token.price, + ), + trendPercentText = token.priceChangePercent, + trendType = trendType, + chartData = null, + isUnder100kMarketCap = token.isUnder100kMarketCap, + stakingRate = token.stakingRateText?.let { TextReference.Str(it) }, + updateTimestamp = token.timestamp, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/RecentSearchTokenWithAppCurrency.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/RecentSearchTokenWithAppCurrency.kt new file mode 100644 index 0000000000..6c7939c071 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/RecentSearchTokenWithAppCurrency.kt @@ -0,0 +1,9 @@ +package com.tangem.features.feed.model.search.converter + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.search.model.RecentSearchToken + +internal data class RecentSearchTokenWithAppCurrency( + val token: RecentSearchToken, + val appCurrency: AppCurrency, +) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateRecentTokenChartTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateRecentTokenChartTransformer.kt new file mode 100644 index 0000000000..ffef6c8505 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/state/transformers/UpdateRecentTokenChartTransformer.kt @@ -0,0 +1,30 @@ +package com.tangem.features.feed.model.search.state.transformers + +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.feed.ui.search.state.SearchContentUM +import com.tangem.features.feed.ui.search.state.SearchUM +import kotlinx.collections.immutable.toImmutableList + +internal class UpdateRecentTokenChartTransformer( + private val tokenId: CryptoCurrency.RawID, + private val chartData: MarketChartRawData, +) : SearchUMTransformer { + + override fun transform(prevState: SearchUM): SearchUM { + val content = prevState.content + if (content !is SearchContentUM.History) return prevState + + val updatedTokens = content.recentTokens.map { token -> + if (token.id == tokenId) { + token.copy(chartData = chartData) + } else { + token + } + }.toImmutableList() + + return prevState.copy( + content = content.copy(recentTokens = updatedTokens), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt index d3800af21f..ffdf82486b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt @@ -24,6 +24,7 @@ import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.* +import com.tangem.domain.models.earn.EarnType import com.tangem.features.feed.ui.earn.components.* import com.tangem.features.feed.ui.earn.state.* import com.tangem.features.feed.ui.feed.state.FeedListSearchBar @@ -641,7 +642,8 @@ private fun previewEarnListItemUM( shouldShowCustomBadge = false, ), earnValue = stringReference("APY 6.54%"), - earnType = stringReference("Yield"), + earnTypeTitle = stringReference("Yield"), + earnType = EarnType.YIELD, onItemClick = {}, ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListItem.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListItem.kt index 6e4e9799cb..66ead3805f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListItem.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListItem.kt @@ -30,6 +30,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.* +import com.tangem.domain.models.earn.EarnType import com.tangem.features.feed.ui.earn.state.EarnListItemUM @Composable @@ -95,7 +96,7 @@ private fun EarnListItemV1(item: EarnListItemUM, modifier: Modifier = Modifier) ) SpacerH(2.dp) Text( - text = item.earnType.resolveReference(), + text = item.earnTypeTitle.resolveReference(), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.caption2, maxLines = 1, @@ -147,6 +148,7 @@ private fun EarnListItemV2(item: EarnListItemUM, modifier: Modifier = Modifier) ModeBlock( modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM), earnType = item.earnType, + earnTypeTitle = item.earnTypeTitle, ) }, ) @@ -200,19 +202,24 @@ private fun TokenTitle(name: String, symbol: String, modifier: Modifier = Modifi } @Composable -private fun ModeBlock(earnType: TextReference, modifier: Modifier = Modifier) { +private fun ModeBlock(earnType: EarnType, earnTypeTitle: TextReference, modifier: Modifier = Modifier) { Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp), ) { Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_staking_new_16), + imageVector = ImageVector.vectorResource( + id = when (earnType) { + EarnType.STAKING -> R.drawable.ic_staking_new_16 + EarnType.YIELD -> R.drawable.ic_yield_mode_16 + }, + ), tint = TangemTheme.colors2.markers.iconGray, contentDescription = null, ) Text( - text = earnType.resolveReference(), + text = earnTypeTitle.resolveReference(), style = TangemTheme.typography2.captionSemibold12, color = TangemTheme.colors2.text.neutral.tertiary, ) @@ -239,7 +246,8 @@ private fun EarnListItemPreviewV1() { shouldShowCustomBadge = false, ), earnValue = stringReference("APY 8.50%"), - earnType = stringReference("Yield"), + earnTypeTitle = stringReference("Yield"), + earnType = EarnType.YIELD, onItemClick = {}, ), ) @@ -257,7 +265,8 @@ private fun EarnListItemPreviewV1() { shouldShowCustomBadge = false, ), earnValue = stringReference("APY 8.50%"), - earnType = stringReference("Yield"), + earnTypeTitle = stringReference("Yield"), + earnType = EarnType.YIELD, onItemClick = {}, ), ) @@ -286,7 +295,8 @@ private fun EarnListItemPreviewV2() { shouldShowCustomBadge = false, ), earnValue = stringReference("APY 8.50%"), - earnType = stringReference("Yield"), + earnTypeTitle = stringReference("Yield"), + earnType = EarnType.YIELD, onItemClick = {}, ), ) @@ -305,7 +315,8 @@ private fun EarnListItemPreviewV2() { shouldShowCustomBadge = false, ), earnValue = stringReference("APY 8.50%"), - earnType = stringReference("Yield"), + earnTypeTitle = stringReference("Yield"), + earnType = EarnType.YIELD, onItemClick = {}, ), ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt index b9c88ee004..1acfb25681 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt @@ -21,6 +21,7 @@ import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.* +import com.tangem.domain.models.earn.EarnType import com.tangem.features.feed.ui.earn.state.EarnListItemUM @Composable @@ -161,7 +162,8 @@ private fun EarnListItemPreviewV1() { shouldShowCustomBadge = false, ), earnValue = stringReference("APY 6.54%"), - earnType = stringReference("Yield"), + earnTypeTitle = stringReference("Yield"), + earnType = EarnType.YIELD, onItemClick = {}, ), onClick = {}, @@ -187,7 +189,8 @@ private fun EarnListItemPreviewV2() { shouldShowCustomBadge = false, ), earnValue = stringReference("APY 6.54%"), - earnType = stringReference("Yield"), + earnTypeTitle = stringReference("Yield"), + earnType = EarnType.YIELD, onItemClick = {}, ), onClick = {}, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt index 72ba54139d..763954d13f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt @@ -3,6 +3,7 @@ package com.tangem.features.feed.ui.earn.state import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.earn.EarnType import kotlinx.collections.immutable.ImmutableList @Immutable @@ -32,6 +33,7 @@ internal data class EarnListItemUM( val tokenName: TextReference, val currencyIconState: CurrencyIconState, val earnValue: TextReference, - val earnType: TextReference, + val earnType: EarnType, + val earnTypeTitle: TextReference, val onItemClick: () -> Unit, ) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt index 7f775602b2..e3a0a86a28 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt @@ -11,12 +11,14 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.earn.EarnType import com.tangem.features.feed.model.market.list.state.SortByTypeUM import com.tangem.features.feed.ui.earn.state.EarnListItemUM import com.tangem.features.feed.ui.earn.state.EarnListUM import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.features.feed.ui.feed.state.* import kotlinx.collections.immutable.* +import java.math.BigDecimal @Suppress("MagicNumber") internal object FeedListPreviewDataProvider { @@ -246,7 +248,11 @@ internal object FeedListPreviewDataProvider { iconUrl = null, ratingPosition = rating, marketCap = marketCap, - price = MarketsListItemUM.Price(text = "31 285.72$"), + price = MarketsListItemUM.Price( + text = "31 285.72$", + fiatPrice = BigDecimal(123123), + annotated = stringReference("31 285.72$"), + ), trendPercentText = percent, trendType = trendType, chartData = MarketChartRawData( @@ -273,7 +279,8 @@ internal object FeedListPreviewDataProvider { shouldShowCustomBadge = false, ), earnValue = stringReference("APY 6.54%"), - earnType = stringReference("Yield"), + earnTypeTitle = stringReference("Yield"), + earnType = EarnType.YIELD, onItemClick = {}, ) }.toPersistentList() diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt index 68076673c5..48b2e71786 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt @@ -37,6 +37,7 @@ import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveAnnotatedReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.LocalRedesignEnabled @@ -182,6 +183,7 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) Column(modifier = Modifier.weight(1f)) { TokenPriceText( price = state.priceText, + priceAnnotated = state.priceAnnotated, triggerPriceChange = state.triggerPriceChange, ) Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) { @@ -212,6 +214,28 @@ private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) @Composable private fun TokenPriceText( + price: String, + triggerPriceChange: StateEvent, + priceAnnotated: TextReference, + modifier: Modifier = Modifier, +) { + if (LocalRedesignEnabled.current) { + TokenPriceTextV2( + priceAnnotated = priceAnnotated, + triggerPriceChange = triggerPriceChange, + modifier = modifier, + ) + } else { + TokenPriceTextV1( + price = price, + triggerPriceChange = triggerPriceChange, + modifier = modifier, + ) + } +} + +@Composable +private fun TokenPriceTextV1( price: String, triggerPriceChange: StateEvent, modifier: Modifier = Modifier, @@ -244,6 +268,40 @@ private fun TokenPriceText( ) } +@Composable +private fun TokenPriceTextV2( + priceAnnotated: TextReference, + triggerPriceChange: StateEvent, + modifier: Modifier = Modifier, +) { + val growColor = TangemTheme.colors2.graphic.status.accent + val fallColor = TangemTheme.colors2.graphic.status.warning + val generalColor = TangemTheme.colors2.text.neutral.primary + + val color = remember(generalColor) { Animatable(generalColor) } + + EventEffect(triggerPriceChange) { priceChangeType -> + val nextColor = when (priceChangeType) { + PriceChangeType.UP, + -> growColor + PriceChangeType.DOWN -> fallColor + PriceChangeType.NEUTRAL -> return@EventEffect + } + + color.animateTo(nextColor, snap()) + color.animateTo(generalColor, tween(durationMillis = 500)) + } + + Text( + text = priceAnnotated.resolveAnnotatedReference(), + modifier = modifier, + color = color.value, + autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography2.headingBold34.fontSize), + maxLines = 1, + style = TangemTheme.typography2.headingBold34, + ) +} + @Composable private fun IntervalSelector( trendInterval: PriceChangeInterval, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/MetricsCards.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/MetricsCards.kt index d485b3bb18..966cf8e307 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/MetricsCards.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/MetricsCards.kt @@ -156,12 +156,42 @@ internal fun FDVCard(item: InfoPointUMV2.FullyDilutedValuation) { modifier = Modifier .heightIn(120.dp) .fillMaxWidth(), - title = { MetricValueText(value = item.value) }, + title = { + if (item.fullyDilutedValuationChange24 != null) { + Row { + MetricValueText(value = item.fullyDilutedValuationChange24) + Text( + modifier = Modifier.padding(TangemTheme.dimens2.x1), + text = stringResourceSafe(R.string.markets_token_details_trading_interval), + style = TangemTheme.typography2.captionSemibold11, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } else { + MetricValueText(value = item.value) + } + }, content = { - InformationTextBlock( - text = resourceReference(R.string.markets_token_details_fully_diluted_valuation), - onInfoClick = item.onInfoClick, - ) + Column { + if (item.fullyDilutedValuationChange24 != null) { + Text( + text = item.value?.resolveReference() + ?: stringResourceSafe(R.string.token_market_metrics_no_data), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + SpacerH(4.dp) + } + + InformationTextBlock( + text = resourceReference(R.string.markets_token_details_fully_diluted_valuation), + onInfoClick = item.onInfoClick, + ) + } }, ) } @@ -445,6 +475,7 @@ private fun MetricsCardsPreview() { FDVCard( item = InfoPointUMV2.FullyDilutedValuation( value = stringReference("$ 1.5 T"), + fullyDilutedValuationChange24 = stringReference("$ 2.44 M in total"), onInfoClick = {}, ), ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt index ed7261d399..63b06d0833 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/preview/MarketsTokenDetailsPreview.kt @@ -50,6 +50,7 @@ internal object MarketsTokenDetailsPreview { onScroll = {}, ), onShareClick = {}, + priceAnnotated = stringReference("$0.00000000324"), ) val contentState = MarketsTokenDetailsUM( @@ -144,5 +145,6 @@ internal object MarketsTokenDetailsPreview { onScroll = {}, ), onShareClick = {}, + priceAnnotated = stringReference("$0.00000000324"), ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt index 7da2ed065b..2cd7078d51 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt @@ -14,6 +14,7 @@ import java.math.BigDecimal internal data class MarketsTokenDetailsUM( val tokenName: String, val priceText: String, + val priceAnnotated: TextReference, val iconUrl: String?, val dateTimeText: TextReference, val priceChangePercentText: String?, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MetricsUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MetricsUM.kt index 76db464700..396e1d12cd 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MetricsUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MetricsUM.kt @@ -38,6 +38,7 @@ internal sealed interface InfoPointUMV2 { @Immutable data class FullyDilutedValuation( val value: TextReference?, + val fullyDilutedValuationChange24: TextReference?, val onInfoClick: () -> Unit, ) : InfoPointUMV2 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt index b2da4c8e5f..287b580b15 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.tangem.common.ui.markets.MarketsListItem import com.tangem.common.ui.markets.MarketsListItemPlaceholder +import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerW @@ -133,7 +134,10 @@ private fun LazyListScope.searchHistoryItems( } } -private fun LazyListScope.searchResultsItems(results: SearchContentUM.Results, onResultMarketTokenClick: () -> Unit) { +private fun LazyListScope.searchResultsItems( + results: SearchContentUM.Results, + onResultMarketTokenClick: (MarketsListItemUM) -> Unit, +) { if (results.userAssets.isNotEmpty()) { item(key = "header_portfolio") { SectionHeader(title = stringResourceSafe(R.string.markets_search_portfolio_header)) @@ -163,7 +167,7 @@ private fun LazyListScope.searchResultsItems(results: SearchContentUM.Results, o private fun LazyListScope.marketSearchResultItems( market: MarketSearchResultUM.Content, hasUserAssetsSection: Boolean, - onResultMarketTokenClick: () -> Unit, + onResultMarketTokenClick: (MarketsListItemUM) -> Unit, ) { if (hasUserAssetsSection) { item(key = "spacer_between_sections") { @@ -185,7 +189,7 @@ private fun LazyListScope.marketSearchResultItems( shape = RoundedCornerShape(TangemTheme.dimens2.x5), ), model = token, - onClick = onResultMarketTokenClick, + onClick = { onResultMarketTokenClick(token) }, ) } if (market.shouldShowUnder100kNotification) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt index b8f37312a4..bfb95e3819 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt @@ -25,6 +25,7 @@ import com.tangem.features.feed.ui.search.state.* import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal /** Labeled UI state for [SearchContent] previews. */ internal data class SearchContentPreviewScenario( @@ -192,7 +193,11 @@ internal object SearchContentPreviewFixtures { iconUrl = null, ratingPosition = rating, marketCap = marketCap, - price = MarketsListItemUM.Price(text = priceText), + price = MarketsListItemUM.Price( + text = priceText, + annotated = stringReference(priceText), + fiatPrice = BigDecimal(123123), + ), trendPercentText = trendText, trendType = trend, chartData = chart, @@ -384,7 +389,7 @@ private val SearchContentPreviewCallbacks = SearchCallbacks( onLoadMore = {}, onClearHintsClick = {}, onTextHintClick = { _ -> }, - onResultMarketTokenClick = {}, + onResultMarketTokenClick = { _ -> }, ) /** All [SearchContentPreviewScenario] values for the Preview Parameter dropdown in Android Studio. */ diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt index e793512ce1..e5641b2bb8 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt @@ -1,8 +1,10 @@ package com.tangem.features.feed.ui.search.state +import com.tangem.common.ui.markets.models.MarketsListItemUM + internal data class SearchCallbacks( val onLoadMore: () -> Unit, val onClearHintsClick: () -> Unit, val onTextHintClick: (hint: String) -> Unit, - val onResultMarketTokenClick: () -> Unit, + val onResultMarketTokenClick: (MarketsListItemUM) -> Unit, ) \ No newline at end of file diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index bc76460d54..78f5e2b66c 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -81,6 +81,7 @@ dependencies { /* Common */ implementation(projects.common.ui) + implementation(projects.common.uiMarkets) implementation(projects.common.uiCharts) implementation(projects.common.routing) 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 1cdaebeff0..e27a121ad5 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 @@ -6,6 +6,7 @@ import com.tangem.common.routing.AppRoute import com.tangem.common.ui.charts.state.MarketChartData import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter import com.tangem.common.ui.charts.state.sorted +import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -62,6 +63,8 @@ internal class TokenMarketBlockModel @Inject constructor( TokenMarketBlockUM( currencySymbol = params.cryptoCurrency.symbol, currentPrice = null, + currentPriceValue = null, + priceAnnotated = null, h24Percent = null, priceChangeType = PriceChangeType.NEUTRAL, chartData = null, @@ -86,15 +89,21 @@ internal class TokenMarketBlockModel @Inject constructor( h24Percent = res.priceChange, ) + val appCurrency = currentAppCurrency.value state.value = state.value.copy( currentPrice = res.fiatRate.format { fiat( // TODO get currency from quotes use case [REDACTED_TASK_KEY] - fiatCurrencyCode = currentAppCurrency.value.code, + fiatCurrencyCode = appCurrency.code, // TODO get currency from quotes use case [REDACTED_TASK_KEY] - fiatCurrencySymbol = currentAppCurrency.value.symbol, + fiatCurrencySymbol = appCurrency.symbol, ).price() }, + currentPriceValue = res.fiatRate, + priceAnnotated = res.fiatRate.toMarketsListItemPriceAnnotated( + appCurrencyCode = appCurrency.code, + appCurrencySymbol = appCurrency.symbol, + ), h24Percent = res.priceChange.format { percent() }, priceChangeType = PriceChangeType.fromBigDecimal(res.priceChange), ) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlockLegacy.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlockLegacy.kt index 17d3314738..820fa36229 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlockLegacy.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlockLegacy.kt @@ -20,6 +20,8 @@ import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.marketprice.PriceChangeInPercent import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -27,6 +29,7 @@ import com.tangem.features.markets.impl.R import com.tangem.features.markets.token.block.impl.model.formatter.toChartType import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal import kotlin.random.Random @Composable @@ -47,6 +50,8 @@ internal fun TokenMarketBlockLegacy(state: TokenMarketBlockUM, modifier: Modifie modifier = Modifier.weight(1f), symbol = state.currencySymbol, priceText = state.currentPrice, + priceValue = state.currentPriceValue, + priceAnnotated = state.priceAnnotated, percentText = state.h24Percent, type = state.priceChangeType, ) @@ -61,11 +66,14 @@ internal fun TokenMarketBlockLegacy(state: TokenMarketBlockUM, modifier: Modifie ) } +@Suppress("LongParameterList") @OptIn(ExperimentalLayoutApi::class) @Composable private fun LeftSide( symbol: String, priceText: String?, + priceValue: BigDecimal?, + priceAnnotated: TextReference?, percentText: String?, type: PriceChangeType, modifier: Modifier = Modifier, @@ -88,6 +96,8 @@ private fun LeftSide( modifier = Modifier.alignByBaseline(), price = priceText, priceChangeType = type, + priceValue = priceValue ?: BigDecimal.ZERO, + priceAnnotated = priceAnnotated ?: TextReference.EMPTY, ) Row( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), @@ -181,6 +191,8 @@ private fun Preview() { priceChangeType = PriceChangeType.UP, chartData = data, onClick = {}, + currentPriceValue = BigDecimal(1234), + priceAnnotated = stringReference("0,5$"), ) TangemThemePreview { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/state/TokenMarketBlockUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/state/TokenMarketBlockUM.kt index 475c39e3f9..5ed0370ffb 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/state/TokenMarketBlockUM.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/state/TokenMarketBlockUM.kt @@ -2,10 +2,14 @@ package com.tangem.features.markets.token.block.impl.ui.state import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.TextReference +import java.math.BigDecimal internal data class TokenMarketBlockUM( val currencySymbol: String, val currentPrice: String?, + val currentPriceValue: BigDecimal?, + val priceAnnotated: TextReference?, val h24Percent: String?, val priceChangeType: PriceChangeType, val chartData: MarketChartRawData?, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/converter/SwapMarketsTokenItemConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/converter/SwapMarketsTokenItemConverter.kt index f76ce2af6d..98a4c621b3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/converter/SwapMarketsTokenItemConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/converter/SwapMarketsTokenItemConverter.kt @@ -5,6 +5,7 @@ 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.common.ui.markets.models.MarketsListItemUM +import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated import com.tangem.core.ui.R import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.resourceReference @@ -119,7 +120,12 @@ internal class SwapMarketsTokenItemConverter( return MarketsListItemUM.Price( text = priceText, + annotated = tokenQuotesShort.currentPrice.toMarketsListItemPriceAnnotated( + appCurrencyCode = appCurrency.code, + appCurrencySymbol = appCurrency.symbol, + ), changeType = changeType, + fiatPrice = tokenQuotesShort.currentPrice, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/converter/SwapMarketsTokenItemConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/converter/SwapMarketsTokenItemConverter.kt index 31ea67416e..0e693eab2d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/converter/SwapMarketsTokenItemConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/converter/SwapMarketsTokenItemConverter.kt @@ -5,6 +5,7 @@ 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.common.ui.markets.models.MarketsListItemUM +import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList @@ -119,7 +120,12 @@ internal class SwapMarketsTokenItemConverter( return MarketsListItemUM.Price( text = priceText, + annotated = tokenQuotesShort.currentPrice.toMarketsListItemPriceAnnotated( + appCurrencyCode = appCurrency.code, + appCurrencySymbol = appCurrency.symbol, + ), changeType = changeType, + fiatPrice = tokenQuotesShort.currentPrice, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt index f61c2e0da4..bb19ee30a7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt @@ -19,6 +19,7 @@ import com.tangem.feature.swap.models.market.state.SwapMarketState import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal internal object SwapSelectTokenPreviewProvider { @@ -205,7 +206,11 @@ internal object SwapSelectTokenPreviewProvider { iconUrl = iconUrl, ratingPosition = ratingPosition, marketCap = marketCap, - price = MarketsListItemUM.Price(text = "31 285.72$"), + price = MarketsListItemUM.Price( + text = "31 285.72$", + annotated = stringReference("31 285.72$"), + fiatPrice = BigDecimal("123123"), + ), trendPercentText = "12.43%", trendType = trendType, chartData = chartData, From 1b6fded61cec261d8b868832211e52307076198a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Apr 2026 13:38:14 +0300 Subject: [PATCH 74/75] Updated on 2026-08-14 --- .../state/ExpressTransactionsBlockState.kt | 7 - .../tokendetails/TokenDetailsDialogConfig.kt | 155 ------------------ .../EmptyExpressTransactionsComponent.kt | 1 - ...reviewEmptyExpressTransactionsComponent.kt | 1 - .../tangempay/ui/TangemPayDetailsScreen.kt | 2 - .../tokendetails/TokenDetailsPreviewData.kt | 2 - .../model/ExpressTransactionsModel.kt | 28 +++- .../model/TokenDetailsClickIntents.kt | 2 - .../tokendetails/model/TokenDetailsModel.kt | 107 +++++++++--- .../tokendetails/state/TokenDetailsState.kt | 2 - .../state/factory/ExpressStateFactory.kt | 35 ---- .../TokenDetailsSkeletonStateConverter.kt | 1 - .../state/factory/TokenDetailsStateFactory.kt | 93 ----------- .../ui/TokenDetailsScreenLegacy.kt | 6 - .../ui/components/TokenDetailsDialogs.kt | 35 ---- 15 files changed, 108 insertions(+), 369 deletions(-) delete mode 100644 common/ui/src/main/java/com/tangem/common/ui/tokendetails/TokenDetailsDialogConfig.kt delete mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionsBlockState.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionsBlockState.kt index bd12e6323b..1d223a1ead 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionsBlockState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionsBlockState.kt @@ -1,7 +1,6 @@ package com.tangem.common.ui.expressStatus.state import androidx.compose.runtime.Composable -import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import kotlinx.collections.immutable.PersistentList @@ -9,15 +8,9 @@ data class ExpressTransactionsBlockState( val transactions: PersistentList, val transactionsToDisplay: PersistentList, val bottomSheetSlot: BottomSheetSlot?, - val dialogSlot: DialogSlot?, ) data class BottomSheetSlot( val config: TangemBottomSheetConfig, val content: @Composable () -> Unit, -) - -data class DialogSlot( - val config: TokenDetailsDialogConfig, - val content: @Composable () -> Unit, ) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokendetails/TokenDetailsDialogConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/tokendetails/TokenDetailsDialogConfig.kt deleted file mode 100644 index 8681f259a6..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/tokendetails/TokenDetailsDialogConfig.kt +++ /dev/null @@ -1,155 +0,0 @@ -package com.tangem.common.ui.tokendetails - -import com.tangem.core.ui.R -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList - -/** - * Wallet bottom sheet config - * - * @property isShow flag that determine if bottom sheet is shown - * @property onDismissRequest lambda be invoked when bottom sheet is dismissed - * @property content content config - */ -data class TokenDetailsDialogConfig( - val isShow: Boolean, - val onDismissRequest: () -> Unit, - val content: DialogContentConfig, -) { - - sealed class DialogContentConfig { - - abstract val title: TextReference? - abstract val message: TextReference - abstract val confirmButtonConfig: ButtonConfig - abstract val cancelButtonConfig: ButtonConfig? - - data class ButtonConfig( - val text: TextReference, - val onClick: () -> Unit, - val hasWarning: Boolean = false, - ) - - data class ConfirmHideConfig( - val currencyTitle: String, - val onConfirmClick: () -> Unit, - val onCancelClick: () -> Unit, - ) : DialogContentConfig() { - override val title: TextReference = TextReference.Res( - id = R.string.token_details_hide_alert_title, - formatArgs = wrappedList(currencyTitle), - ) - - override val message: TextReference = TextReference.Res(R.string.token_details_hide_alert_message) - - override val cancelButtonConfig: ButtonConfig = ButtonConfig( - text = TextReference.Res(R.string.common_cancel), - onClick = onCancelClick, - ) - - override val confirmButtonConfig: ButtonConfig = ButtonConfig( - text = TextReference.Res(R.string.token_details_hide_alert_hide), - onClick = onConfirmClick, - hasWarning = true, - ) - } - - data class HasLinkedTokensConfig( - val currencyName: String, - val currencySymbol: String, - val networkName: String, - val onConfirmClick: () -> Unit, - ) : DialogContentConfig() { - override val title: TextReference = TextReference.Res( - id = R.string.token_details_unable_hide_alert_title, - formatArgs = wrappedList(currencySymbol), - ) - - override val message: TextReference = TextReference.Res( - id = R.string.token_details_unable_hide_alert_message, - formatArgs = wrappedList(currencyName, currencySymbol, networkName), - ) - - override val cancelButtonConfig: ButtonConfig? - get() = null - - override val confirmButtonConfig: ButtonConfig = ButtonConfig( - text = TextReference.Res(R.string.common_ok), - onClick = onConfirmClick, - ) - } - - data class DisabledButtonReasonDialogConfig( - val text: TextReference, - val onConfirmClick: () -> Unit, - ) : DialogContentConfig() { - - override val title = null - - override val message: TextReference = text - - override val cancelButtonConfig = null - - override val confirmButtonConfig: ButtonConfig = ButtonConfig( - text = TextReference.Res(R.string.common_ok), - onClick = onConfirmClick, - ) - } - - data class RemoveIncompleteTransactionConfirmDialogConfig( - val onConfirmClick: () -> Unit, - val onCancelClick: () -> Unit, - ) : DialogContentConfig() { - override val title = null - - override val message: TextReference = TextReference.Res( - id = R.string.warning_kaspa_unfinished_token_transaction_discard_message, - ) - - override val cancelButtonConfig: ButtonConfig = ButtonConfig( - text = TextReference.Res(R.string.common_cancel), - onClick = onCancelClick, - ) - - override val confirmButtonConfig: ButtonConfig = ButtonConfig( - text = TextReference.Res(R.string.common_yes), - onClick = onConfirmClick, - ) - } - - data class ErrorDialogConfig( - val text: TextReference, - val onConfirmClick: () -> Unit, - ) : DialogContentConfig() { - - override val title = null - - override val message: TextReference = text - - override val cancelButtonConfig = null - - override val confirmButtonConfig: ButtonConfig = ButtonConfig( - text = TextReference.Res(R.string.common_ok), - onClick = onConfirmClick, - ) - } - - data class ConfirmExpressStatusHideDialogConfig( - val onConfirmClick: () -> Unit, - val onCancelClick: () -> Unit, - ) : DialogContentConfig() { - override val title: TextReference = resourceReference(R.string.express_status_hide_dialog_title) - override val message: TextReference = resourceReference(R.string.express_status_hide_dialog_text) - override val confirmButtonConfig: ButtonConfig = ButtonConfig( - text = resourceReference(R.string.common_hide), - onClick = onConfirmClick, - ) - - override val cancelButtonConfig: ButtonConfig = ButtonConfig( - text = resourceReference(R.string.common_cancel), - onClick = onCancelClick, - ) - } - } -} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/express/EmptyExpressTransactionsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/express/EmptyExpressTransactionsComponent.kt index e8eeac64b5..47eccba438 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/express/EmptyExpressTransactionsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/express/EmptyExpressTransactionsComponent.kt @@ -29,7 +29,6 @@ internal class EmptyExpressTransactionsComponent( transactions = persistentListOf(), transactionsToDisplay = persistentListOf(), bottomSheetSlot = null, - dialogSlot = null, ) } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/express/PreviewEmptyExpressTransactionsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/express/PreviewEmptyExpressTransactionsComponent.kt index ee4fe81f30..a1d6ba6634 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/express/PreviewEmptyExpressTransactionsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/express/PreviewEmptyExpressTransactionsComponent.kt @@ -27,7 +27,6 @@ internal class PreviewEmptyExpressTransactionsComponent : ExpressTransactionsCom transactions = persistentListOf(), transactionsToDisplay = persistentListOf(), bottomSheetSlot = null, - dialogSlot = null, ) } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt index 11e52a71c4..1e3a51c9a4 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt @@ -74,7 +74,6 @@ internal fun TangemPayDetailsScreen( val cardDetailsState by cardDetailsBlockComponent.state.collectAsStateWithLifecycle() val expressState by expressTransactionsComponent.state.collectAsStateWithLifecycle() val expressTransactionsBottomSheetState = expressState.bottomSheetSlot - val expressTransactionsDialogState = expressState.dialogSlot TangemPullToRefreshContainer( config = state.pullToRefreshConfig, @@ -158,7 +157,6 @@ internal fun TangemPayDetailsScreen( with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryState) } } } - expressTransactionsDialogState?.content() expressTransactionsBottomSheetState?.content() } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index fa2e9d6908..e7ada8cb1c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -169,7 +169,6 @@ internal object TokenDetailsPreviewData { marketPriceBlockState = marketPriceLoading, stakingBlocksState = stakingLoadingBlock, notifications = persistentListOf(), - dialogConfig = null, expressTxs = persistentListOf(), expressTxsToDisplay = persistentListOf(), pullToRefreshConfig = pullToRefreshConfig, @@ -194,7 +193,6 @@ internal object TokenDetailsPreviewData { ), stakingBlocksState = stakingAvailableBlock, notifications = persistentListOf(), - dialogConfig = null, expressTxs = persistentListOf(), expressTxsToDisplay = persistentListOf(), pullToRefreshConfig = pullToRefreshConfig, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/ExpressTransactionsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/ExpressTransactionsModel.kt index bd24cb6cc5..bdeb591c42 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/ExpressTransactionsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/ExpressTransactionsModel.kt @@ -5,9 +5,15 @@ import arrow.core.getOrElse import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.features.tokendetails.impl.R import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -36,6 +42,7 @@ import kotlin.coroutines.cancellation.CancellationException @ModelScoped internal class ExpressTransactionsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, paramsContainer: ParamsContainer, expressStatusFactory: ExpressStatusFactory.Factory, getUserWalletUseCase: GetUserWalletUseCase, @@ -66,7 +73,6 @@ internal class ExpressTransactionsModel @Inject constructor( private val stateFactory by lazy(mode = LazyThreadSafetyMode.NONE) { ExpressStateFactory( currentStateProvider = currentStateProvider, - expressTransactionsClickIntents = this, ) } @@ -109,7 +115,21 @@ internal class ExpressTransactionsModel @Inject constructor( } override fun onConfirmDisposeExpressStatus() { - internalUiState.value = stateFactory.getStateWithConfirmHideExpressStatus() + uiMessageSender.send( + DialogMessage( + title = resourceReference(R.string.express_status_hide_dialog_title), + message = resourceReference(R.string.express_status_hide_dialog_text), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_hide), + onClick = { + onDisposeExpressStatus() + }, + ) + }, + secondActionBuilder = { cancelAction() }, + ), + ) } override fun onDisposeExpressStatus() { @@ -136,10 +156,6 @@ internal class ExpressTransactionsModel @Inject constructor( internalUiState.value = stateFactory.getStateWithClosedBottomSheet() } - override fun onDismissDialog() { - internalUiState.value = stateFactory.getStateWithClosedDialog() - } - override fun onDestroy() { clear() super.onDestroy() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt index bc6d1d4b7e..af6e80b86e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt @@ -101,8 +101,6 @@ interface ExpressTransactionsClickIntents { fun onDisposeExpressStatus() fun onDismissBottomSheet() - - fun onDismissDialog() } @Suppress("TooManyFunctions") diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 1adbb5051c..1aec75edd1 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -26,6 +26,7 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener +import com.tangem.common.ui.tokens.getUnavailabilityReasonText import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState @@ -36,6 +37,8 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.status.usecase.IsCryptoCurrencyCouldHideUseCase @@ -739,10 +742,6 @@ internal class TokenDetailsModel @Inject constructor( } } - override fun onDismissDialog() { - internalUiState.value = stateFactory.getStateWithClosedDialog() - } - override fun onHideClick() { analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonRemoveToken(cryptoCurrency.symbol)) @@ -752,10 +751,10 @@ internal class TokenDetailsModel @Inject constructor( cryptoCurrency = cryptoCurrency, ) - internalUiState.value = if (canHide) { - stateFactory.getStateWithConfirmHideTokenDialog(cryptoCurrency) + if (canHide) { + showConfirmHideTokenDialog(cryptoCurrency) } else { - stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency) + showLinkedTokensDialog(cryptoCurrency) } } } @@ -968,7 +967,7 @@ internal class TokenDetailsModel @Inject constructor( } } if (message != null) { - internalUiState.value = stateFactory.getStateWithErrorDialog(stringReference(message)) + showErrorDialog(stringReference(message)) TangemLogger.e(message) } }, @@ -1012,7 +1011,7 @@ internal class TokenDetailsModel @Inject constructor( } if (message != null) { - internalUiState.value = stateFactory.getStateWithErrorDialog(message) + showErrorDialog(message) } }, ifRight = { internalUiState.value = stateFactory.getStateWithRemovedRequiredTrustlineNotification() }, @@ -1027,9 +1026,7 @@ internal class TokenDetailsModel @Inject constructor( blockchain = cryptoCurrency.network.name, ), ) - modelScope.launch { - internalUiState.value = stateFactory.getStateWithDismissIncompleteTransactionConfirmDialog() - } + showDismissIncompleteTransactionConfirmDialog() } override fun onConfirmDismissIncompleteTransactionClick() { @@ -1039,9 +1036,7 @@ internal class TokenDetailsModel @Inject constructor( currency = cryptoCurrency, ).fold( ifLeft = { e -> - internalUiState.value = stateFactory.getStateWithErrorDialog( - stringReference(e.message.orEmpty()), - ) + showErrorDialog(stringReference(e.message.orEmpty())) TangemLogger.e("Error: $e") }, ifRight = { @@ -1066,7 +1061,7 @@ internal class TokenDetailsModel @Inject constructor( ifLeft = { e -> when (e) { is AssociateAssetError.NotEnoughBalance -> { - internalUiState.value = stateFactory.getStateWithErrorDialog( + showErrorDialog( resourceReference( id = R.string.warning_hedera_token_association_not_enough_hbar_message, formatArgs = wrappedList(e.feeCurrency.symbol), @@ -1074,9 +1069,7 @@ internal class TokenDetailsModel @Inject constructor( ) } is AssociateAssetError.DataError -> { - internalUiState.value = stateFactory.getStateWithErrorDialog( - stringReference(e.message.orEmpty()), - ) + showErrorDialog(stringReference(e.message.orEmpty())) TangemLogger.e("Error: $e") } } @@ -1091,7 +1084,7 @@ internal class TokenDetailsModel @Inject constructor( } override fun onConfirmDisposeExpressStatus() { - internalUiState.value = stateFactory.getStateWithConfirmHideExpressStatus() + showConfirmHideExpressStatusDialog() } override fun onDisposeExpressStatus() { @@ -1125,7 +1118,7 @@ internal class TokenDetailsModel @Inject constructor( private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean { if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false - internalUiState.value = stateFactory.getStateWithActionButtonErrorDialog(unavailabilityReason) + showErrorDialog(unavailabilityReason.getUnavailabilityReasonText()) return true } @@ -1154,6 +1147,78 @@ internal class TokenDetailsModel @Inject constructor( uiMessageSender.send(SnackbarMessage(resourceReference(R.string.staking_error_no_validators_title))) } + private fun showConfirmHideTokenDialog(currency: CryptoCurrency) { + uiMessageSender.send( + DialogMessage( + title = resourceReference( + id = R.string.token_details_hide_alert_title, + formatArgs = wrappedList(currency.name), + ), + message = resourceReference(R.string.token_details_hide_alert_message), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.token_details_hide_alert_hide), + isWarning = true, + onClick = ::onHideConfirmed, + ) + }, + secondActionBuilder = { cancelAction() }, + ), + ) + } + + private fun showLinkedTokensDialog(currency: CryptoCurrency) { + uiMessageSender.send( + DialogMessage( + title = resourceReference( + id = R.string.token_details_unable_hide_alert_title, + formatArgs = wrappedList(currency.symbol), + ), + message = resourceReference( + id = R.string.token_details_unable_hide_alert_message, + formatArgs = wrappedList(currency.name, currency.symbol, currency.network.name), + ), + ), + ) + } + + private fun showDismissIncompleteTransactionConfirmDialog() { + uiMessageSender.send( + DialogMessage( + message = resourceReference(R.string.warning_kaspa_unfinished_token_transaction_discard_message), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_yes), + onClick = ::onConfirmDismissIncompleteTransactionClick, + ) + }, + secondActionBuilder = { cancelAction() }, + ), + ) + } + + private fun showConfirmHideExpressStatusDialog() { + uiMessageSender.send( + DialogMessage( + title = resourceReference(R.string.express_status_hide_dialog_title), + message = resourceReference(R.string.express_status_hide_dialog_text), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_hide), + onClick = { + onDisposeExpressStatus() + }, + ) + }, + secondActionBuilder = { cancelAction() }, + ), + ) + } + + private fun showErrorDialog(text: TextReference) { + uiMessageSender.send(DialogMessage(message = text)) + } + private fun checkForActionUpdates() { combine( tokenDetailsDeepLinkActionListener.tokenDetailsActionFlow, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 8fc3962fe6..721e190ecc 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -1,7 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM -import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState @@ -18,7 +17,6 @@ internal data class TokenDetailsState( val notifications: ImmutableList, val expressTxsToDisplay: PersistentList, val expressTxs: PersistentList, - val dialogConfig: TokenDetailsDialogConfig?, val pullToRefreshConfig: PullToRefreshConfig, val bottomSheetConfig: TangemBottomSheetConfig?, val isBalanceHidden: Boolean, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt index 950bd50f41..c3b8678b1a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt @@ -1,17 +1,11 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory -import androidx.compose.runtime.Composable -import com.tangem.common.ui.expressStatus.state.DialogSlot import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState -import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig -import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsClickIntents -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs import com.tangem.utils.Provider import kotlinx.collections.immutable.persistentListOf internal class ExpressStateFactory( private val currentStateProvider: Provider, - private val expressTransactionsClickIntents: ExpressTransactionsClickIntents, ) { fun getInitialState(): ExpressTransactionsBlockState { @@ -19,40 +13,11 @@ internal class ExpressStateFactory( transactions = persistentListOf(), transactionsToDisplay = persistentListOf(), bottomSheetSlot = null, - dialogSlot = null, ) } - fun getStateWithClosedDialog(): ExpressTransactionsBlockState { - val state = currentStateProvider() - return state.copy(dialogSlot = null) - } - fun getStateWithClosedBottomSheet(): ExpressTransactionsBlockState { val state = currentStateProvider() return state.copy(bottomSheetSlot = null) } - - fun getStateWithConfirmHideExpressStatus(): ExpressTransactionsBlockState { - return currentStateProvider().copy( - dialogSlot = TokenDetailsDialogConfig( - isShow = true, - onDismissRequest = expressTransactionsClickIntents::onDismissDialog, - content = TokenDetailsDialogConfig.DialogContentConfig.ConfirmExpressStatusHideDialogConfig( - onConfirmClick = { - expressTransactionsClickIntents.onDisposeExpressStatus() - expressTransactionsClickIntents.onDismissDialog() - }, - onCancelClick = expressTransactionsClickIntents::onDismissDialog, - ), - ).toDialogSlot(), - ) - } - - private fun TokenDetailsDialogConfig.toDialogSlot(): DialogSlot { - val contentLambda: @Composable () -> Unit = { - TokenDetailsDialogs(this) - } - return DialogSlot(config = this, content = contentLambda) - } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 5580bcc639..649b2b064a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -65,7 +65,6 @@ internal class TokenDetailsSkeletonStateConverter( notifications = persistentListOf(), expressTxs = persistentListOf(), expressTxsToDisplay = persistentListOf(), - dialogConfig = null, pullToRefreshConfig = createPullToRefresh(), bottomSheetConfig = null, isBalanceHidden = true, 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 aa14a1aa20..fe79d03388 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 @@ -2,8 +2,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import arrow.core.Either import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig -import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig -import com.tangem.common.ui.tokens.getUnavailabilityReasonText import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.extensions.TextReference @@ -20,7 +18,6 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -124,79 +121,6 @@ internal class TokenDetailsStateFactory( return tokenDetailsButtonsConverter.convert(actions) } - fun getStateWithClosedDialog(): TokenDetailsState { - val state = currentStateProvider() - return state.copy(dialogConfig = state.dialogConfig?.copy(isShow = false)) - } - - fun getStateWithConfirmHideTokenDialog(currency: CryptoCurrency): TokenDetailsState { - return currentStateProvider().copy( - dialogConfig = TokenDetailsDialogConfig( - isShow = true, - onDismissRequest = expressTransactionsClickIntents::onDismissDialog, - content = TokenDetailsDialogConfig.DialogContentConfig.ConfirmHideConfig( - currencyTitle = currency.name, - onConfirmClick = tokenDetailsClickIntents::onHideConfirmed, - onCancelClick = expressTransactionsClickIntents::onDismissDialog, - ), - ), - ) - } - - fun getStateWithLinkedTokensDialog(currency: CryptoCurrency): TokenDetailsState { - return currentStateProvider().copy( - dialogConfig = TokenDetailsDialogConfig( - isShow = true, - onDismissRequest = expressTransactionsClickIntents::onDismissDialog, - content = TokenDetailsDialogConfig.DialogContentConfig.HasLinkedTokensConfig( - currencyName = currency.name, - currencySymbol = currency.symbol, - networkName = currency.network.name, - onConfirmClick = expressTransactionsClickIntents::onDismissDialog, - ), - ), - ) - } - - fun getStateWithDismissIncompleteTransactionConfirmDialog(): TokenDetailsState { - return currentStateProvider().copy( - dialogConfig = TokenDetailsDialogConfig( - isShow = true, - onDismissRequest = expressTransactionsClickIntents::onDismissDialog, - content = TokenDetailsDialogConfig.DialogContentConfig.RemoveIncompleteTransactionConfirmDialogConfig( - onConfirmClick = tokenDetailsClickIntents::onConfirmDismissIncompleteTransactionClick, - onCancelClick = expressTransactionsClickIntents::onDismissDialog, - ), - ), - ) - } - - fun getStateWithActionButtonErrorDialog(unavailabilityReason: ScenarioUnavailabilityReason): TokenDetailsState { - return currentStateProvider().copy( - dialogConfig = TokenDetailsDialogConfig( - isShow = true, - onDismissRequest = expressTransactionsClickIntents::onDismissDialog, - content = TokenDetailsDialogConfig.DialogContentConfig.DisabledButtonReasonDialogConfig( - text = unavailabilityReason.getUnavailabilityReasonText(), - onConfirmClick = expressTransactionsClickIntents::onDismissDialog, - ), - ), - ) - } - - fun getStateWithErrorDialog(text: TextReference): TokenDetailsState { - return currentStateProvider().copy( - dialogConfig = TokenDetailsDialogConfig( - isShow = true, - onDismissRequest = expressTransactionsClickIntents::onDismissDialog, - content = TokenDetailsDialogConfig.DialogContentConfig.ErrorDialogConfig( - text = text, - onConfirmClick = expressTransactionsClickIntents::onDismissDialog, - ), - ), - ) - } - fun getRefreshingState(): TokenDetailsState { return refreshStateConverter.convert(true) } @@ -259,7 +183,6 @@ internal class TokenDetailsStateFactory( val state = currentStateProvider() return state.copy( notifications = notificationConverter.removeKaspaIncompleteTransactionWarning(state), - dialogConfig = state.dialogConfig?.copy(isShow = false), ) } @@ -302,22 +225,6 @@ internal class TokenDetailsStateFactory( ) } - fun getStateWithConfirmHideExpressStatus(): TokenDetailsState { - return currentStateProvider().copy( - dialogConfig = TokenDetailsDialogConfig( - isShow = true, - onDismissRequest = expressTransactionsClickIntents::onDismissDialog, - content = TokenDetailsDialogConfig.DialogContentConfig.ConfirmExpressStatusHideDialogConfig( - onConfirmClick = { - expressTransactionsClickIntents.onDisposeExpressStatus() - expressTransactionsClickIntents.onDismissDialog() - }, - onCancelClick = expressTransactionsClickIntents::onDismissDialog, - ), - ), - ) - } - private fun TokenDetailsAppBarMenuConfig.updateMenu( userWallet: UserWallet, hasDerivations: Boolean, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreenLegacy.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreenLegacy.kt index da6b9c913b..cecd5b8b08 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreenLegacy.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreenLegacy.kt @@ -30,7 +30,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBl import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlockLegacy -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheet @@ -62,7 +61,6 @@ internal fun TokenDetailsScreenLegacy( ) { scaffoldPaddings -> val listState = rememberLazyListState() val txHistoryComponentState by txHistoryComponent.txHistoryState.collectAsStateWithLifecycle() - val dialogConfig = state.dialogConfig val betweenItemsPadding = TangemTheme.dimens.spacing12 val horizontalPadding = TangemTheme.dimens.spacing16 val itemModifier = Modifier @@ -161,10 +159,6 @@ internal fun TokenDetailsScreenLegacy( } } - if (dialogConfig != null) { - TokenDetailsDialogs(dialogConfig = dialogConfig) - } - state.bottomSheetConfig?.let { config -> when (config.content) { is ChooseAddressBottomSheetConfig -> { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt deleted file mode 100644 index 5428e5d949..0000000000 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components - -import androidx.compose.runtime.Composable -import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.extensions.resolveReference - -@Composable -internal fun TokenDetailsDialogs(dialogConfig: TokenDetailsDialogConfig) { - if (dialogConfig.isShow) { - TokenDetailsDialog(config = dialogConfig) - } -} - -@Composable -private fun TokenDetailsDialog(config: TokenDetailsDialogConfig) { - BasicDialog( - message = config.content.message.resolveReference(), - confirmButton = DialogButtonUM( - title = config.content.confirmButtonConfig.text.resolveReference(), - isWarning = config.content.confirmButtonConfig.hasWarning, - onClick = config.content.confirmButtonConfig.onClick, - ), - onDismissDialog = config.onDismissRequest, - title = config.content.title?.resolveReference(), - dismissButton = config.content.cancelButtonConfig?.let { cancelButtonConfig -> - DialogButtonUM( - title = cancelButtonConfig.text.resolveReference(), - isWarning = cancelButtonConfig.hasWarning, - onClick = cancelButtonConfig.onClick, - ) - }, - ) -} \ No newline at end of file From 25504da416481a765c6c2a6b023f75988db41afe Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Apr 2026 16:28:26 +0300 Subject: [PATCH 75/75] Updated on 2026-08-14 --- .../java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt | 4 +++- .../transformers/converter/WalletTokensListUMConverter.kt | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt index 67d8918c7f..c5950b2f4a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt @@ -17,6 +17,7 @@ import com.tangem.core.ui.ds.row.TangemRowContainer import com.tangem.core.ui.ds.row.TangemRowLayoutId import com.tangem.core.ui.ds.row.internal.TangemRowTail import com.tangem.core.ui.ds.row.token.internal.* +import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.test.TokenElementsTestTags @@ -131,6 +132,8 @@ fun TangemTokenRow( titleComponent: @Composable (Modifier) -> Unit, ) { TangemRowContainer( + modifier = modifier + .clickableSingle(enabled = tokenRowUM.onItemClick != null, onClick = { tokenRowUM.onItemClick?.invoke() }), content = { headComponent( Modifier @@ -193,7 +196,6 @@ fun TangemTokenRow( .testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK), ) }, - modifier = modifier, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt index aa24a27e24..58e67d0012 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt @@ -81,8 +81,10 @@ internal class WalletTokensListUMConverter( .asSequence() .flatMap { accountStatus -> if (isAccountsModeEnabled) { - val isCollapsable = accountStatus.tokenList.flattenCurrencies().isNotEmpty() - val isExpanded = expandedAccounts.contains(accountStatus.account.accountId) + val currencies = accountStatus.tokenList.flattenCurrencies() + val isCollapsable = currencies.isNotEmpty() + val isExpanded = + currencies.isEmpty() || expandedAccounts.contains(accountStatus.account.accountId) sequenceOf( TokensListItemUM2.Portfolio( tokenRowUM = accountRowConverter.convert(accountStatus),