From 6b05c5566ebc497a29b02f0c7f77619e059e74b1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Feb 2026 15:15:31 +0300 Subject: [PATCH 001/106] Updated on 2026-08-14 --- .../kotlin/com/tangem/common/BaseTestCase.kt | 4 +- .../common/extensions/CustomAssertsExt.kt | 20 +++ .../tangem/screens/MainScreenPageObject.kt | 5 + .../com/tangem/screens/MarketsPageObject.kt | 5 + .../screens/MarketsTokenDetailsPageObject.kt | 33 ++++ .../screens/SwapSelectTokenPageObject.kt | 70 +++++++++ .../com/tangem/screens/SwapTokenPageObject.kt | 5 + .../TokenDetailsScreenActionButtonsTest.kt | 23 ++- .../tests/swap/SwapSelectTokenScreenTest.kt | 72 +++++++++ .../com/tangem/tests/swap/SwapStoriesTest.kt | 141 ++++++++++++++++++ .../SwapTokenScreenTest.kt} | 61 +++++++- .../actions/ActionButtonTestSemantics.kt | 10 ++ .../ui/components/buttons/actions/Actions.kt | 8 +- .../core/ui/components/fields/SearchBar.kt | 7 +- .../MarketTokenDetailsBottomSheetTestTags.kt | 8 + .../tangem/core/ui/test/SearchBarTestTags.kt | 7 + .../ui/test/SwapSelectTokenScreenTestTags.kt | 6 + .../core/ui/test/SwapTokenScreenTestTags.kt | 1 + .../portfolio/impl/ui/PortfolioItem.kt | 3 + .../impl/ui/PortfolioQuickActions.kt | 43 +++--- .../features/onramp/swap/ui/ExchangeCard.kt | 6 +- .../com/tangem/feature/swap/ui/SwapScreen.kt | 6 +- 22 files changed, 492 insertions(+), 52 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/MarketsTokenDetailsPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/SwapSelectTokenPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/swap/SwapSelectTokenScreenTest.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt rename app/src/androidTest/kotlin/com/tangem/tests/{SwapTokenTest.kt => swap/SwapTokenScreenTest.kt} (80%) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonTestSemantics.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/MarketTokenDetailsBottomSheetTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/SearchBarTestTags.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/SwapSelectTokenScreenTestTags.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 009af28e09..df3927d4fb 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -147,7 +147,9 @@ abstract class BaseTestCase : TestCase( "NEW_ONRAMP_MAIN_ENABLED" to true, "HOT_WALLET_ENABLED" to true, "YIELD_SUPPLY_FEATURE_ENABLED" to true, - "ACCOUNTS_FEATURE_ENABLED" to true + "ACCOUNTS_FEATURE_ENABLED" to true, + "FEED_ENABLED" to true, + "GASLESS_TRANSACTIONS_ENABLED" to true, ) ) } diff --git a/app/src/androidTest/kotlin/com/tangem/common/extensions/CustomAssertsExt.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/CustomAssertsExt.kt index e1040b8ffc..827136c346 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/extensions/CustomAssertsExt.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/CustomAssertsExt.kt @@ -1,5 +1,9 @@ package com.tangem.common.extensions +import androidx.compose.ui.test.SemanticsMatcher +import com.tangem.common.utils.LazyListItemNode +import com.tangem.core.ui.components.buttons.actions.HasBadgeKey +import com.tangem.core.ui.components.buttons.actions.IsDimmedKey import io.github.kakaocup.compose.node.element.KNode fun assertElementDoesNotExist( @@ -22,4 +26,20 @@ fun assertElementDoesNotExist( throw e } } +} + +fun Any.assertIsDimmed(expectedValue: Boolean = true) { + val matcher = SemanticsMatcher.expectValue(IsDimmedKey, expectedValue) + when (this) { + is KNode, is LazyListItemNode -> this.assert(matcher) + else -> throw IllegalArgumentException("Unsupported type: ${this::class}") + } +} + +fun Any.assertHasBadge(expectedValue: Boolean = true) { + val matcher = SemanticsMatcher.expectValue(HasBadgeKey, expectedValue) + when (this) { + is KNode, is LazyListItemNode -> this.assert(matcher) + else -> throw IllegalArgumentException("Unsupported type: ${this::class}") + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt index d1ce4ef047..8d412d40e7 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt @@ -235,6 +235,11 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) useUnmergedTree = true } + val searchThroughMarketPlaceholder: KNode = child { + hasText(getResourceString(R.string.markets_search_header_title)) + useUnmergedTree = true + } + fun tokenNetworkGroupTitle(tokenNetwork: String): KNode { return lazyList.child { hasTestTag(MainScreenTestTags.TOKEN_LIST_ITEM) diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MarketsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MarketsPageObject.kt index 4f6707a071..cad5a0cdea 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MarketsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MarketsPageObject.kt @@ -48,6 +48,11 @@ class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : useUnmergedTree = true } + val searchThroughMarketPlaceholder: KNode = child { + hasText(getResourceString(R.string.markets_search_header_title)) + useUnmergedTree = true + } + fun tokenWithTitle(title: String): KNode { return lazyList.child { hasText(title) diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MarketsTokenDetailsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MarketsTokenDetailsPageObject.kt new file mode 100644 index 0000000000..237a0f183b --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/MarketsTokenDetailsPageObject.kt @@ -0,0 +1,33 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.MarketTokenDetailsBottomSheetTestTags +import com.tangem.core.ui.test.TokenElementsTestTags +import com.tangem.features.onramp.impl.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.hasTestTag as withTestTag +import androidx.compose.ui.test.hasText as withText + +class MarketsTokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val swapPortfolioQuickActionButton: KNode = child { + hasTestTag(MarketTokenDetailsBottomSheetTestTags.PORTFOLIO_QUICK_ACTION_BUTTON) + hasText(getResourceString(R.string.common_swap), substring = true) + } + + fun tokenWithTitle(title: String): KNode = child { + hasAnyAncestor(withTestTag(MarketTokenDetailsBottomSheetTestTags.PORTFOLIO_TOKEN_ITEM)) + hasTestTag(TokenElementsTestTags.TOKEN_TITLE) + hasAnySibling(withTestTag(TokenElementsTestTags.TOKEN_ICON)) + hasAnyChild(withText(title)) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onMarketsTokenDetailsScreen(function: MarketsTokenDetailsPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectTokenPageObject.kt new file mode 100644 index 0000000000..83e09a9a09 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectTokenPageObject.kt @@ -0,0 +1,70 @@ +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.SearchBarTestTags +import com.tangem.core.ui.test.SwapSelectTokenScreenTestTags +import com.tangem.core.ui.test.TokenElementsTestTags +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 +import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasText as withText + +class SwapSelectTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasTestTag(TopAppBarTestTags.TITLE) + hasText(getResourceString(R.string.common_swap)) + useUnmergedTree = true + } + + val closeButton: KNode = child { + hasTestTag(TopAppBarTestTags.CLOSE_BUTTON) + } + + val youSwapTitle: KNode = child { + hasText(getResourceString(R.string.swapping_from_title)) + useUnmergedTree = true + } + + val youSwapBlock: KNode = child { + hasTestTag(SwapSelectTokenScreenTestTags.YOU_SWAP_BLOCK) + hasAnyDescendant(withText(getResourceString(R.string.action_buttons_you_want_to_swap))) + useUnmergedTree = true + } + + val youReceiveTitle: KNode = child { + hasText(getResourceString(R.string.swapping_to_title)) + useUnmergedTree = true + } + + val youReceiveBlock: KNode = child { + hasTestTag(SwapSelectTokenScreenTestTags.YOU_SWAP_BLOCK) + hasAnyDescendant(withText(getResourceString(R.string.action_buttons_you_want_to_receive))) + useUnmergedTree = true + } + + val searchBarIcon: KNode = child { + hasTestTag(SearchBarTestTags.ICON) + useUnmergedTree = true + } + + val searchBarPlaceholderText: KNode = child { + hasTestTag(SearchBarTestTags.PLACEHOLDER_TEXT) + useUnmergedTree = true + } + + fun tokenWithName(tokenName: String): KNode = child { + hasTestTag(TokenElementsTestTags.TOKEN_TITLE) + hasAnyChild(withText(tokenName)) + useUnmergedTree = true + } + +} + +internal fun BaseTestCase.onSwapSelectTokenScreen(function: SwapSelectTokenPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt index 188c9ece2e..f31becbd3f 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt @@ -13,6 +13,10 @@ import androidx.compose.ui.test.hasTestTag as withTestTag class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { + val container: KNode = child { + hasTestTag(SwapTokenScreenTestTags.CONTAINER) + } + val title: KNode = child { hasTestTag(TopAppBarTestTags.TITLE) hasText(getResourceString(R.string.common_swap)) @@ -78,6 +82,7 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) fun tokenSymbol(symbol: String): KNode = child { hasTestTag(SwapTokenScreenTestTags.TOKEN_SYMBOL) + hasText(symbol) } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt index d14e14e295..309a1e637c 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt @@ -3,6 +3,7 @@ package com.tangem.tests.actionButtons import com.tangem.common.BaseTestCase import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.extensions.SwipeDirection +import com.tangem.common.extensions.assertIsDimmed import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.extensions.swipeVertical import com.tangem.common.utils.resetWireMockScenarioState @@ -60,8 +61,6 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() { @Test fun checkActionButtonsStateTest() { val tokenTitle = "Bitcoin" - val actionButtonIsNotDimmed = "Action button is not dimmed" - val actionButtonIsDimmed = "Action button is dimmed" setupHooks().run { step("Open 'Main Screen'") { @@ -75,19 +74,19 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() { onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() } } step("Assert 'Receive' button is not dimmed") { - onTokenDetailsScreen { receiveButton().assertContentDescriptionEquals(actionButtonIsNotDimmed) } + onTokenDetailsScreen { receiveButton().assertIsDimmed(false) } } step("Assert 'Buy' button is not dimmed") { - onTokenDetailsScreen { buyButton().assertContentDescriptionEquals(actionButtonIsNotDimmed) } + onTokenDetailsScreen { buyButton().assertIsDimmed(false) } } step("Assert 'Send' button is not dimmed") { - onTokenDetailsScreen { sendButton().assertContentDescriptionEquals(actionButtonIsNotDimmed) } + onTokenDetailsScreen { sendButton().assertIsDimmed(false) } } step("Assert 'Swap' button is dimmed") { - onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) } + onTokenDetailsScreen { swapButton().assertIsDimmed() } } step("Assert 'Sell' button is dimmed") { - onTokenDetailsScreen { sellButton().assertContentDescriptionEquals(actionButtonIsDimmed) } + onTokenDetailsScreen { sellButton().assertIsDimmed() } } } } @@ -130,7 +129,6 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() { @Test fun checkSwapButtonProviderErrorTest() { val tokenTitle = "POL (ex-MATIC)" - val actionButtonIsDimmed = "Action button is dimmed" setupHooks().run { step("Open 'Main Screen'") { @@ -147,7 +145,7 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() { onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() } } step("Assert 'Swap' button is dimmed") { - onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) } + onTokenDetailsScreen { swapButton().assertIsDimmed() } } step("Click on 'Swap' button") { onTokenDetailsScreen { swapButton().performClick() } @@ -162,7 +160,7 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() { onSwapIsNotSupportedDialog { okButton.performClick() } } step("Assert 'Swap' button is dimmed") { - onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) } + onTokenDetailsScreen { swapButton().assertIsDimmed() } } } } @@ -172,7 +170,6 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() { @Test fun checkSwapButtonExpressErrorTest() { val tokenTitle = "Polygon" - val actionButtonIsDimmed = "Action button is dimmed" val scenarioName = "express_api_assets" val scenarioState = "Error" @@ -198,7 +195,7 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() { onMainScreen { tokenWithTitleAndAddress(tokenTitle).performClick() } } step("Assert 'Swap' button is dimmed") { - onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) } + onTokenDetailsScreen { swapButton().assertIsDimmed() } } step("Click on 'Swap' button") { onTokenDetailsScreen { swapButton().performClick() } @@ -213,7 +210,7 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() { onOperationIsUnavailableDialog { okButton.performClick() } } step("Assert 'Swap' button is dimmed") { - onTokenDetailsScreen { swapButton().assertContentDescriptionEquals(actionButtonIsDimmed) } + onTokenDetailsScreen { swapButton().assertIsDimmed() } } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapSelectTokenScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapSelectTokenScreenTest.kt new file mode 100644 index 0000000000..e474212036 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapSelectTokenScreenTest.kt @@ -0,0 +1,72 @@ +package com.tangem.tests.swap + +import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.synchronizeAddresses +import com.tangem.screens.onMainScreen +import com.tangem.screens.onSwapSelectTokenScreen +import com.tangem.screens.onSwapStoriesScreen +import com.tangem.screens.onSwapTokenScreen +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 SwapSelectTokenScreenTest : BaseTestCase() { + + @AllureId("2829") + @DisplayName("Open 'Swap select token' screen from 'Main' screen") + @Test + fun openSwapSelectTokenScreenFromMainScreenTest() { + val swapTokenName = "Ethereum" + val receiveTokenName = "Polygon" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on 'Swap' button") { + onMainScreen { swapButton.performClick() } + } + step("Close 'Stories' screen") { + onSwapStoriesScreen { closeButton.clickWithAssertion() } + } + step("Assert 'Swap select token' screen title is displayed") { + onSwapSelectTokenScreen { title.assertIsDisplayed() } + } + step("Assert 'You swap' title is displayed") { + onSwapSelectTokenScreen { youSwapTitle.assertIsDisplayed() } + } + step("Assert 'You swap' block is displayed") { + onSwapSelectTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Assert search icon is displayed") { + onSwapSelectTokenScreen { searchBarIcon.assertIsDisplayed() } + } + step("Assert search placeholder is displayed") { + onSwapSelectTokenScreen { searchBarPlaceholderText.assertIsDisplayed() } + } + step("Click on token with name '$swapTokenName'") { + onSwapSelectTokenScreen { tokenWithName(swapTokenName).performClick() } + } + step("Assert 'You receive' title is displayed") { + onSwapSelectTokenScreen { youReceiveTitle.assertIsDisplayed() } + } + step("Assert 'You receive' block is displayed") { + onSwapSelectTokenScreen { youReceiveBlock.assertIsDisplayed() } + } + step("Click on token with name '$receiveTokenName'") { + onSwapSelectTokenScreen { tokenWithName(receiveTokenName).performClick() } + } + step("Assert 'Swap token' screen is opened") { + onSwapTokenScreen { container.assertIsDisplayed() } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt new file mode 100644 index 0000000000..ccbca2b78c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt @@ -0,0 +1,141 @@ +package com.tangem.tests.swap + +import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.assertHasBadge +import com.tangem.common.extensions.clickWithAssertion +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 SwapStoriesTest : BaseTestCase() { + + @AllureId("5453") + @DisplayName("Check 'Swap' button badge on 'Main' screen") + @Test + fun checkMainScreenSwapButtonBadgeTest() { + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Assert 'Swap' button has badge") { + onMainScreen { swapButton.assertHasBadge() } + } + step("Click on 'Swap' button") { + onMainScreen { swapButton.performClick() } + } + step("Close 'Stories' screen") { + onSwapStoriesScreen { closeButton.clickWithAssertion() } + } + step("Assert 'Swap' screen title is displayed") { + onSwapTokenScreen { title.assertIsDisplayed() } + } + step("Click on 'Close' button") { + onSwapTokenScreen { closeButton.performClick() } + } + step("Assert 'Swap' button has not badge") { + onMainScreen { swapButton.assertHasBadge(false) } + } + } + } + + @AllureId("5454") + @DisplayName("Check 'Swap' button badge on token details screen") + @Test + fun checkTokenDetailsScreenSwapButtonTest() { + val tokenName = "Ethereum" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() } + } + step("Assert 'Swap' button has badge") { + onTokenDetailsScreen { swapButton().assertHasBadge() } + } + step("Click on 'Swap' button") { + onTokenDetailsScreen { swapButton().performClick() } + } + step("Close 'Stories' screen") { + onSwapStoriesScreen { closeButton.clickWithAssertion() } + } + step("Assert 'Swap' screen title is displayed") { + onSwapTokenScreen { title.assertIsDisplayed() } + } + step("Click on 'Close' button") { + onSwapTokenScreen { closeButton.performClick() } + } + step("Assert 'Swap' button has not badge") { + onTokenDetailsScreen { swapButton().assertHasBadge(false) } + } + } + } + + @AllureId("5455") + @DisplayName("Check 'Swap' button badge on token details in 'Market' screen") + @Test + fun checkMarketTokenDetailsScreenSwapButtonTest() { + val tokenName = "Ethereum" + val badgeShown = "Badge shown" + val badgeHidden = "Badge hidden" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Markets' screen") { + onMainScreen { searchThroughMarketPlaceholder.performClick() } + waitForIdle() + } + step("") { + onMarketsScreen { searchThroughMarketPlaceholder.performClick() } + } + step("Click on $tokenName token") { + waitForIdle() + onMarketsScreen { tokenWithTitle(tokenName).clickWithAssertion() } + } + step("Click on $tokenName token") { + waitForIdle() + onMarketsTokenDetailsScreen { tokenWithTitle(tokenName).performClick() } + } + step("Assert 'Swap' button has badge") { + onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertIsDisplayed() } + onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeShown) } + } + step("Click on 'Swap' button") { + onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.performClick() } + } + step("Close 'Stories' screen") { + onSwapStoriesScreen { closeButton.clickWithAssertion() } + } + step("Assert 'Swap' screen title is displayed") { + onSwapTokenScreen { title.assertIsDisplayed() } + } + step("Click on 'Close' button") { + onSwapTokenScreen { closeButton.performClick() } + } + step("Assert 'Swap' button has not badge") { + onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeHidden) } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt similarity index 80% rename from app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt rename to app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt index f57ae57734..d0f574aa0b 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/SwapTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt @@ -1,4 +1,4 @@ -package com.tangem.tests +package com.tangem.tests.swap import androidx.compose.ui.test.hasText import com.tangem.common.BaseTestCase @@ -19,7 +19,7 @@ import io.qameta.allure.kotlin.junit4.DisplayName import org.junit.Test @HiltAndroidTest -class SwapTokenTest : BaseTestCase() { +class SwapTokenScreenTest : BaseTestCase() { @ApiEnv( ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) @@ -43,7 +43,7 @@ class SwapTokenTest : BaseTestCase() { step("Click on token with name: '$tokenTitle'") { onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } } - step("Click on token with name: '$tokenTitle'") { + step("Assert title: '$tokenTitle' is displayed") { onTokenDetailsScreen { title.assertIsDisplayed() } } step("Click on 'Swap' button") { @@ -123,7 +123,7 @@ class SwapTokenTest : BaseTestCase() { step("Click on token with name: '$tokenTitle'") { onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } } - step("Click on token with name: '$tokenTitle'") { + step("Assert title: '$tokenTitle' is displayed") { onTokenDetailsScreen { title.assertIsDisplayed() } } step("Turn off Wi-Fi and Mobile Data") { @@ -171,7 +171,7 @@ class SwapTokenTest : BaseTestCase() { step("Click on token with name: '$tokenTitle'") { onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } } - step("Click on token with name: '$tokenTitle'") { + step("Assert title: '$tokenTitle' is displayed") { onTokenDetailsScreen { title.assertIsDisplayed() } } step("Click on 'Swap' button") { @@ -239,4 +239,55 @@ class SwapTokenTest : BaseTestCase() { } } } + + @AllureId("2828") + @DisplayName("Swap: network fee") + @Test + fun goToTokenSwapTest() { + val swapTokenSymbol = "POL" + val receiveTokenSymbol = "ETH" + val tokenTitle = "Polygon" + + setupHooks().run { + + resetWireMockScenarios() + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Click on 'Swap' button") { + onTokenDetailsScreen { swapButton().performClick() } + } + step("Close 'Stories' screen") { + onSwapStoriesScreen { closeButton.clickWithAssertion() } + } + step("Assert 'Swap' screen title is displayed") { + onSwapTokenScreen { title.assertIsDisplayed() } + } + step("Assert 'Close' button is displayed") { + onSwapTokenScreen { closeButton.assertIsDisplayed() } + } + step("Assert 'Swap tokens on screen' button is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + swapTokensOnscreenButton.assertIsDisplayed() + } + } + } + step("Assert token symbol: '$swapTokenSymbol' is displayed") { + onSwapTokenScreen { tokenSymbol(swapTokenSymbol).assertIsDisplayed() } + } + step("Assert token symbol: '$receiveTokenSymbol' is displayed") { + onSwapTokenScreen { tokenSymbol(receiveTokenSymbol).assertIsDisplayed() } + } + } + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonTestSemantics.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonTestSemantics.kt new file mode 100644 index 0000000000..feb66508f5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/ActionButtonTestSemantics.kt @@ -0,0 +1,10 @@ +package com.tangem.core.ui.components.buttons.actions + +import androidx.compose.ui.semantics.SemanticsPropertyKey +import androidx.compose.ui.semantics.SemanticsPropertyReceiver + +val IsDimmedKey = SemanticsPropertyKey("IsDimmed") +val HasBadgeKey = SemanticsPropertyKey("HasBadge") + +var SemanticsPropertyReceiver.isDimmed by IsDimmedKey +var SemanticsPropertyReceiver.hasBadge by HasBadgeKey \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt index ef86d84493..cbeee40848 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/actions/Actions.kt @@ -22,7 +22,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource -import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -136,11 +135,8 @@ fun ActionBaseButton( } .clip(shape) .semantics { - contentDescription = if (config.shouldDimContent) { - "Action button is dimmed" - } else { - "Action button is not dimmed" - } + isDimmed = config.shouldDimContent + hasBadge = config.shouldShowBadge } .combinedClickable( enabled = config.isEnabled, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt index 17ddadc6ff..2e031c6e58 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt @@ -38,6 +38,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.BaseSearchBarTestTags +import com.tangem.core.ui.test.SearchBarTestTags @Composable fun SearchBar( @@ -126,7 +127,9 @@ private fun DecorationBox( contentPadding = PaddingValues(all = TangemTheme.dimens.spacing12), leadingIcon = { Icon( - modifier = Modifier.size(TangemTheme.dimens.size20), + modifier = Modifier + .size(TangemTheme.dimens.size20) + .testTag(SearchBarTestTags.ICON), painter = painterResource(id = R.drawable.ic_search_24), tint = TangemTheme.colors.icon.informative, contentDescription = null, @@ -137,6 +140,7 @@ private fun DecorationBox( state = state, focusManager = focusManager, keyboardController = keyboardController, + modifier = Modifier.testTag(SearchBarTestTags.CLEAR_BUTTON), ) }, placeholder = { @@ -146,6 +150,7 @@ private fun DecorationBox( style = TangemTheme.typography.body2, maxLines = 1, overflow = TextOverflow.Ellipsis, + modifier = Modifier.testTag(SearchBarTestTags.PLACEHOLDER_TEXT), ) }, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/MarketTokenDetailsBottomSheetTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/MarketTokenDetailsBottomSheetTestTags.kt new file mode 100644 index 0000000000..0df0d21488 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/MarketTokenDetailsBottomSheetTestTags.kt @@ -0,0 +1,8 @@ +package com.tangem.core.ui.test + +object MarketTokenDetailsBottomSheetTestTags { + const val PORTFOLIO_QUICK_ACTION_BUTTON = "MARKET_TOKEN_DETAILS_BOTTOM_SHEET_PORTFOLIO_QUICK_ACTION_BUTTON" + const val PORTFOLIO_QUICK_ACTION_BUTTON_TITLE = "MARKET_TOKEN_DETAILS_BOTTOM_SHEET_PORTFOLIO_QUICK_ACTION_BUTTON_TITLE" + const val PORTFOLIO_QUICK_ACTION_BUTTON_ICON = "MARKET_TOKEN_DETAILS_BOTTOM_SHEET_PORTFOLIO_QUICK_ACTION_BUTTON_ICON" + const val PORTFOLIO_TOKEN_ITEM = "MARKET_TOKEN_DETAILS_BOTTOM_SHEET_PORTFOLIO_TOKEN_ITEM" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SearchBarTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SearchBarTestTags.kt new file mode 100644 index 0000000000..2ce2f7a7dc --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SearchBarTestTags.kt @@ -0,0 +1,7 @@ +package com.tangem.core.ui.test + +object SearchBarTestTags { + const val ICON = "SEARCH_BAR_ICON" + const val CLEAR_BUTTON = "SEARCH_BAR_CLEAR_BUTTON" + const val PLACEHOLDER_TEXT = "SEARCH_BAR_PLACEHOLDER_TEXT" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SwapSelectTokenScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SwapSelectTokenScreenTestTags.kt new file mode 100644 index 0000000000..9583009691 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwapSelectTokenScreenTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object SwapSelectTokenScreenTestTags { + const val YOU_SWAP_BLOCK = "SWAP_SELECT_TOKEN_SCREEN_YOU_SWAP_BLOCK" + const val CHOOSE_TOKEN_TEXT = "SWAP_SELECT_TOKEN_SCREEN_CHOOSE_TOKEN_TEXT" +} \ No newline at end of file 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 c64a02bb85..ec8883b577 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 @@ -1,6 +1,7 @@ package com.tangem.core.ui.test object SwapTokenScreenTestTags { + const val CONTAINER = "SWAP_TOKEN_SCREEN_CONTAINER" const val SWAP_BLOCK_HEADER = "SWAP_TOKEN_SCREEN_SWAP_BLOCK" const val BALANCE = "SWAP_TOKEN_SCREEN_BALANCE" const val SWAP_TEXT_FIELD = "SWAP_TOKEN_SCREEN_SWAP_TEXT_FIELD" diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/PortfolioItem.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/PortfolioItem.kt index 9faba4479e..7d605e621d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/PortfolioItem.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/PortfolioItem.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider @@ -17,6 +18,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.MarketTokenDetailsBottomSheetTestTags import com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM import com.tangem.features.feed.impl.R @@ -49,6 +51,7 @@ internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifie start = TangemTheme.dimens.spacing10, end = TangemTheme.dimens.spacing12, ), + modifier = Modifier.testTag(MarketTokenDetailsBottomSheetTestTags.PORTFOLIO_TOKEN_ITEM), ) PortfolioQuickActions( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/PortfolioQuickActions.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/PortfolioQuickActions.kt index ffed264514..2260f52c5d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/PortfolioQuickActions.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/PortfolioQuickActions.kt @@ -1,45 +1,29 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.ui import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.* import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.Canvas import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredSize -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp @@ -51,6 +35,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.MarketTokenDetailsBottomSheetTestTags import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.QuickActionUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -151,7 +136,8 @@ private fun AnimatedVisibilityScope.QuickActionItem( onClick() }, ) - .padding(horizontal = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing4), + .padding(horizontal = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing4) + .testTag(MarketTokenDetailsBottomSheetTestTags.PORTFOLIO_QUICK_ACTION_BUTTON), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18), ) { @@ -168,6 +154,7 @@ private fun AnimatedVisibilityScope.QuickActionItem( text = state.title.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.primary1, + modifier = Modifier.testTag(MarketTokenDetailsBottomSheetTestTags.PORTFOLIO_QUICK_ACTION_BUTTON_TITLE), ) Text( text = state.description.resolveReference(), @@ -192,6 +179,13 @@ private fun AnimatedVisibilityScope.QuickActionIcon(state: QuickActionUM) { shape = CircleShape, ) .size(TangemTheme.dimens.size32) + .semantics { + contentDescription = if (state is QuickActionUM.Exchange && state.shouldShowBadge) { + "Badge shown" + } else { + "Badge hidden" + } + } .drawWithContent { drawContent() if (state is QuickActionUM.Exchange && state.shouldShowBadge) { @@ -202,7 +196,8 @@ private fun AnimatedVisibilityScope.QuickActionIcon(state: QuickActionUM) { ) { Icon( modifier = Modifier - .requiredSize(TangemTheme.dimens.size16), + .requiredSize(TangemTheme.dimens.size16) + .testTag(MarketTokenDetailsBottomSheetTestTags.PORTFOLIO_QUICK_ACTION_BUTTON_ICON), imageVector = ImageVector.vectorResource(id = state.icon), contentDescription = null, tint = TangemTheme.colors.button.primary, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt index 4ee9b3eb0e..da564d9d1b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt @@ -15,6 +15,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.platform.testTag import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -30,6 +31,7 @@ import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.SwapSelectTokenScreenTestTags import com.tangem.core.ui.utils.dashedBorder import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.swap.entity.ExchangeCardUM @@ -50,7 +52,8 @@ internal fun ExchangeCard(state: ExchangeCardUM, isBalanceHidden: Boolean, modif .fillMaxWidth() .heightIn(min = 116.dp) .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.primary), + .background(TangemTheme.colors.background.primary) + .testTag(SwapSelectTokenScreenTestTags.YOU_SWAP_BLOCK), verticalArrangement = Arrangement.SpaceBetween, ) { Title( @@ -157,6 +160,7 @@ private fun EmptyTokenBlock(text: TextReference, modifier: Modifier = Modifier) maxLines = 1, overflow = TextOverflow.Ellipsis, style = TangemTheme.typography.body2, + modifier = Modifier.testTag(SwapSelectTokenScreenTestTags.CHOOSE_TOKEN_TEXT), ) } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index 78ad44f4f9..0a5f77d83d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -8,11 +8,13 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import com.tangem.common.ui.bottomsheet.permission.GiveTxPermissionBottomSheet import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.SwapTokenScreenTestTags import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent import com.tangem.feature.swap.models.SwapStateHolder @@ -50,7 +52,9 @@ internal fun SwapScreen(stateHolder: SwapStateHolder, feeSelectorBlockComponent: } else { null }, - modifier = Modifier.padding(scaffoldPaddings), + modifier = Modifier + .padding(scaffoldPaddings) + .testTag(SwapTokenScreenTestTags.CONTAINER), ) if (stateHolder.bottomSheetConfig != null) { From f626613f806cf842bd185d1dc6a3e932293a6e10 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Jan 2026 19:33:43 +0400 Subject: [PATCH 002/106] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 6 - .../java/com/tangem/tap/LockTimerWorker.kt | 14 +- .../com/tangem/tap/LockUserWalletsTimer.kt | 50 +- .../main/java/com/tangem/tap/MainActivity.kt | 10 - .../java/com/tangem/tap/TangemApplication.kt | 9 - .../com/tangem/tap/common/redux/AppReducer.kt | 2 - .../com/tangem/tap/common/redux/AppState.kt | 4 - .../common/redux/legacy/LegacyMiddleware.kt | 19 +- .../tap/data/RuntimeUserWalletsStore.kt | 39 -- .../tap/di/data/UserWalletsStoreModule.kt | 15 +- .../tangem/tap/di/domain/CardDomainModule.kt | 14 +- .../tap/di/domain/CardLegacyDomainModule.kt | 10 +- .../tap/di/domain/MarketsDomainModule.kt | 6 - .../tap/di/domain/WalletsDomainModule.kt | 140 +----- .../di/UserWalletsListManagerModule.kt | 80 +--- .../BiometricUserWalletsListManager.kt | 434 ------------------ .../GeneralUserWalletsListManager.kt | 203 -------- .../RuntimeUserWalletsListManager.kt | 115 ----- .../BiometricFailReasonConverter.kt | 39 -- .../BiometricUserWalletsKeysRepository.kt | 207 --------- .../tap/domain/userWalletList/utils/Mapper.kt | 2 - .../details/redux/DetailsMiddleware.kt | 125 +---- .../features/details/redux/DetailsReducer.kt | 20 - .../features/details/redux/DetailsState.kt | 8 +- .../appsettings/AppSettingsDialogsFactory.kt | 20 - .../ui/appsettings/AppSettingsItemsFactory.kt | 16 - .../ui/appsettings/AppSettingsScreen.kt | 1 - .../components/SettingsAlertDialog.kt | 6 +- .../ui/appsettings/model/AppSettingsModel.kt | 101 +--- .../ui/resetcard/model/ResetCardModel.kt | 16 +- .../component/DefaultWelcomeComponent.kt | 36 -- .../welcome/component/WelcomeComponent.kt | 14 - .../component/impl/PreviewWelcomeComponent.kt | 20 - .../features/welcome/di/ComponentModule.kt | 18 - .../tap/features/welcome/di/ModelModule.kt | 20 - .../features/welcome/model/WelcomeModel.kt | 140 ------ .../features/welcome/redux/WelcomeAction.kt | 22 - .../welcome/redux/WelcomeMiddleware.kt | 164 ------- .../features/welcome/redux/WelcomeReducer.kt | 36 -- .../features/welcome/redux/WelcomeState.kt | 10 - .../features/welcome/ui/WelcomeScreenState.kt | 15 - .../welcome/ui/components/WarningDialog.kt | 136 ------ .../welcome/ui/components/WelcomeScreen.kt | 101 ---- .../ui/components/WelcomeScreenContent.kt | 113 ----- .../features/welcome/ui/model/WarningModel.kt | 16 - .../tap/network/auth/DefaultAuthProvider.kt | 15 +- .../tangem/tap/network/auth/di/AuthModule.kt | 12 +- .../tap/proxy/redux/DaggerGraphState.kt | 4 - .../component/impl/DefaultRoutingComponent.kt | 22 +- .../tangem/tap/routing/utils/ChildFactory.kt | 23 +- .../BiometricUserWalletsListManagerTest.kt | 216 --------- .../configs/feature_toggles_config.json | 8 - .../feedback/DefaultFeedbackRepository.kt | 16 +- .../tangem/data/feedback/di/FeedbackModule.kt | 6 - .../pay/DefaultTangemPayEligibilityManager.kt | 16 +- .../repository/DefaultOnboardingRepository.kt | 12 +- .../data/wallets/DefaultWalletsRepository.kt | 20 +- ...FilterAvailableNetworksForWalletUseCase.kt | 15 +- .../DefaultUserWalletsSyncDelegate.kt | 45 +- .../wallets/legacy/UserWalletsListError.kt | 42 -- .../UserWalletsListManagerExtensions.kt | 56 --- .../wallets/models/UnlockWalletsError.kt | 12 - .../wallets/repository/WalletsRepository.kt | 9 - .../wallets/usecase/DeleteWalletUseCase.kt | 22 +- .../usecase/GenerateWalletNameUseCase.kt | 13 +- .../usecase/GetSavedWalletsCountUseCase.kt | 22 +- .../usecase/GetSelectedWalletSyncUseCase.kt | 17 +- .../usecase/GetSelectedWalletUseCase.kt | 17 +- .../wallets/usecase/GetUserWalletUseCase.kt | 15 +- .../wallets/usecase/GetWalletNamesUseCase.kt | 11 +- ...letsForAutomaticallyPushEnablingUseCase.kt | 10 +- .../wallets/usecase/GetWalletsUseCase.kt | 17 +- .../wallets/usecase/IsNeedToBackupUseCase.kt | 11 +- .../usecase/IsWalletAlreadySavedUseCase.kt | 18 +- .../wallets/usecase/SaveWalletUseCase.kt | 70 +-- .../wallets/usecase/SelectWalletUseCase.kt | 26 +- .../ShouldSaveUserWalletsSyncUseCase.kt | 9 - .../usecase/ShouldSaveUserWalletsUseCase.kt | 9 - .../wallets/usecase/UnlockWalletsUseCase.kt | 45 -- .../wallets/usecase/UpdateWalletUseCase.kt | 40 +- .../GetSavedWalletsCountUseCaseTest.kt | 141 ------ .../biometry/impl/model/AskBiometryModel.kt | 29 +- .../CreateWalletStartModel.kt | 3 - .../entity/CreateWalletStartUM.kt | 1 - .../ui/CreateWalletStartContent.kt | 123 +++-- .../preview/PreviewDetailsComponent.kt | 2 - .../features/details/model/DetailsModel.kt | 24 +- .../details/model/UserWalletListModel.kt | 79 ++-- .../features/details/utils/UserWalletSaver.kt | 10 +- .../home/impl/DefaultHomeComponent.kt | 8 +- .../features/home/impl/model/HomeModel.kt | 14 +- .../com/tangem/features/home/impl/ui/Home.kt | 23 +- .../home/impl/ui/compose/StoriesScreen.kt | 271 ----------- .../hotwallet/HotWalletFeatureToggles.kt | 2 - .../DefaultHotWalletFeatureToggles.kt | 8 +- .../hotwallet/di/HotWalletFeatureModule.kt | 13 +- .../hotwallet/di/MnemonicRepositoryModule.kt | 18 + .../entry/impl/model/OnboardingEntryModel.kt | 28 +- .../utils/WalletCardItemDelegate.kt | 24 +- .../wallet/child/wallet/model/WalletModel.kt | 73 ++- .../model/intents/WalletCardClickIntents.kt | 76 --- .../intents/WalletContentClickIntents.kt | 36 +- .../intents/WalletWarningsClickIntents.kt | 82 +--- .../domain/WalletNameMigrationUseCase.kt | 32 +- .../implementors/MultiWalletContentLoader.kt | 11 - .../MultiWalletContentLoaderFactory.kt | 6 - .../MultiWalletContentLoaderV2.kt | 10 - .../implementors/SingleWalletContentLoader.kt | 10 - .../SingleWalletContentLoaderFactory.kt | 6 - .../SingleWalletContentLoaderV2.kt | 10 - .../SingleWalletWithTokenContentLoader.kt | 15 +- ...ngleWalletWithTokenContentLoaderFactory.kt | 6 - .../SingleWalletWithTokenContentLoaderV2.kt | 15 +- .../wallet/state/model/WalletAlertState.kt | 7 - .../SetWalletCardDropDownItemsTransformer.kt | 73 --- .../state/utils/WalletLoadingStateFactory.kt | 47 +- .../WalletDropDownItemsSubscriber.kt | 45 -- .../welcome/impl/model/WelcomeModel.kt | 2 +- 118 files changed, 391 insertions(+), 4505 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricFailReasonConverter.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/component/DefaultWelcomeComponent.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/component/impl/PreviewWelcomeComponent.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/di/ComponentModule.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/di/ModelModule.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/welcome/ui/model/WarningModel.kt delete mode 100644 app/src/test/kotlin/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManagerTest.kt delete mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListError.kt delete mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt delete mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/models/UnlockWalletsError.kt delete mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsSyncUseCase.kt delete mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsUseCase.kt delete mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt delete mode 100644 domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt delete mode 100644 features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreen.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/di/MnemonicRepositoryModule.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWalletCardDropDownItemsTransformer.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletDropDownItemsSubscriber.kt diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index c40ccb2d5a..97f825c6ba 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -40,9 +40,7 @@ import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.hot.sdk.TangemHotSdk import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler @@ -92,8 +90,6 @@ interface ApplicationEntryPoint { fun getOneTimeEventFilter(): OneTimeEventFilter - fun getGeneralUserWalletsListManager(): UserWalletsListManager - fun getWasTwinsOnboardingShownUseCase(): WasTwinsOnboardingShownUseCase fun getSaveTwinsOnboardingShownUseCase(): SaveTwinsOnboardingShownUseCase @@ -151,8 +147,6 @@ interface ApplicationEntryPoint { fun getTangemHotSdk(): TangemHotSdk - fun getHotWalletFeatureToggles(): HotWalletFeatureToggles - fun getWcInitializeUseCase(): WcInitializeUseCase fun getTrackingContextProxy(): TrackingContextProxy diff --git a/app/src/main/java/com/tangem/tap/LockTimerWorker.kt b/app/src/main/java/com/tangem/tap/LockTimerWorker.kt index 85497cc56b..62c9fa652d 100644 --- a/app/src/main/java/com/tangem/tap/LockTimerWorker.kt +++ b/app/src/main/java/com/tangem/tap/LockTimerWorker.kt @@ -6,9 +6,6 @@ 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.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.legacy.asLockable -import com.tangem.features.hotwallet.HotWalletFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedInject import timber.log.Timber @@ -18,21 +15,12 @@ class LockTimerWorker @AssistedInject constructor( @Assisted context: Context, @Assisted params: WorkerParameters, private val settingsRepository: SettingsRepository, - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { Timber.i("onStart job") - if (hotWalletFeatureToggles.isHotWalletEnabled) { - userWalletsListRepository.lockAllWallets() - .onRight { - settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) - } - } else { - val userWalletsListManagerLockable = userWalletsListManager.asLockable() ?: return Result.failure() - userWalletsListManagerLockable.lock() + userWalletsListRepository.lockAllWallets().onRight { settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true) } Timber.i("onStart job complete") diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index 32ea789431..809f2808bc 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -9,10 +9,7 @@ import androidx.work.WorkManager import com.tangem.common.routing.AppRoute import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.LockTimerWorker.Companion.TAG import com.tangem.tap.common.extensions.dispatchNavigationAction import kotlinx.coroutines.CoroutineScope @@ -22,16 +19,13 @@ import kotlinx.coroutines.launch import timber.log.Timber import java.util.concurrent.TimeUnit import kotlin.time.Duration -import kotlin.time.Duration.Companion.minutes @Suppress("LongParameterList") internal class LockUserWalletsTimer( private val context: Context, private val settingsRepository: SettingsRepository, private val duration: Duration = with(Duration) { 5.minutes }, - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val coroutineScope: CoroutineScope, private val clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase, ) : LifecycleOwner by context as LifecycleOwner, @@ -59,9 +53,7 @@ internal class LockUserWalletsTimer( ) if (shouldOpenWelcomeScreenOnResume) { - if (hotWalletFeatureToggles.isHotWalletEnabled) { - clearAllHotWalletContextualUnlockUseCase.invoke() - } + clearAllHotWalletContextualUnlockUseCase.invoke() store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false) } @@ -115,38 +107,18 @@ internal class LockUserWalletsTimer( * This job used only when app is foreground when background use [JobScheduler] */ private fun createDelayJob(): Job = coroutineScope.launch { - val startTime = System.currentTimeMillis() - delay(duration) - if (hotWalletFeatureToggles.isHotWalletEnabled) { - val userWallets = userWalletsListRepository.userWalletsSync() - if (userWallets.isNotEmpty()) { - userWalletsListRepository.lockAllWallets() - .onLeft { - start() - } - .onRight { - clearAllHotWalletContextualUnlockUseCase.invoke() - store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } - } - } - } else { - val userWalletsListManager = userWalletsListManager.asLockable() ?: return@launch - - if (userWalletsListManager.hasUserWallets) { - val currentTime = System.currentTimeMillis() - - Timber.i( - """ - Finished - |- Millis passed: ${currentTime - startTime} - """.trimIndent(), - ) - - userWalletsListManager.lock() - store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } - } + val userWallets = userWalletsListRepository.userWalletsSync() + if (userWallets.isNotEmpty()) { + userWalletsListRepository.lockAllWallets() + .onLeft { + start() + } + .onRight { + clearAllHotWalletContextualUnlockUseCase.invoke() + store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } + } } } } \ 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 076a21b682..cf540d0ec0 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -43,9 +43,7 @@ import com.tangem.domain.settings.SetGoogleServicesAvailabilityUseCase import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.SendUnsubmittedHashesUseCase -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.tester.api.TesterMenuLauncher import com.tangem.google.GoogleServicesHelper @@ -114,9 +112,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject lateinit var analyticsEventsHandler: AnalyticsEventHandler - @Inject - lateinit var userWalletsListManager: UserWalletsListManager - @Inject @RootAppComponentContext internal lateinit var rootComponentContext: AppComponentContext @@ -163,9 +158,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var userWalletsListRepository: UserWalletsListRepository - @Inject - internal lateinit var hotWalletFeatureToggles: HotWalletFeatureToggles - @Inject internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase @@ -258,10 +250,8 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { lockUserWalletsTimer = LockUserWalletsTimer( context = this, settingsRepository = settingsRepository, - userWalletsListManager = userWalletsListManager, coroutineScope = mainScope, userWalletsListRepository = userWalletsListRepository, - hotWalletFeatureToggles = hotWalletFeatureToggles, clearAllHotWalletContextualUnlockUseCase = clearAllHotWalletContextualUnlockUseCase, ) diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 6b59b1b8b2..a22cc0dd22 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -58,7 +58,6 @@ import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.operations.attestation.api.TangemApiServiceSettings @@ -146,9 +145,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val oneTimeEventFilter: OneTimeEventFilter get() = entryPoint.getOneTimeEventFilter() - private val generalUserWalletsListManager: UserWalletsListManager - get() = entryPoint.getGeneralUserWalletsListManager() - private val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase get() = entryPoint.getWasTwinsOnboardingShownUseCase() @@ -238,9 +234,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val tangemHotSdk get() = entryPoint.getTangemHotSdk() - private val hotWalletFeatureToggles - get() = entryPoint.getHotWalletFeatureToggles() - private val wcInitializeUseCase get() = entryPoint.getWcInitializeUseCase() @@ -382,7 +375,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. appThemeModeRepository = appThemeModeRepository, balanceHidingRepository = balanceHidingRepository, walletsRepository = walletsRepository, - generalUserWalletsListManager = generalUserWalletsListManager, wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase, saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase, cardRepository = cardRepository, @@ -407,7 +399,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. userTokensResponseStore = userTokensResponseStore, userWalletsListRepository = userWalletsListRepository, tangemHotSdk = tangemHotSdk, - hotWalletFeatureToggles = hotWalletFeatureToggles, trackingContextProxy = trackingContextProxy, ), ), diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 65315fc4cd..bc16aae955 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -2,7 +2,6 @@ package com.tangem.tap.common.redux import com.tangem.tap.common.redux.global.globalReducer import com.tangem.tap.features.details.redux.DetailsReducer -import com.tangem.tap.features.welcome.redux.WelcomeReducer import com.tangem.tap.proxy.redux.DaggerGraphReducer import org.rekotlin.Action @@ -12,7 +11,6 @@ fun appReducer(action: Action, state: AppState): AppState { return AppState( globalState = globalReducer(action, state), detailsState = DetailsReducer.reduce(action, state), - welcomeState = WelcomeReducer.reduce(action, state), daggerGraphState = DaggerGraphReducer.reduce(action, state), ) } 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 7c3065b0f4..b43504d06c 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 @@ -7,8 +7,6 @@ import com.tangem.tap.features.details.redux.DetailsMiddleware import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware -import com.tangem.tap.features.welcome.redux.WelcomeMiddleware -import com.tangem.tap.features.welcome.redux.WelcomeState import com.tangem.tap.proxy.redux.DaggerGraphMiddleware import com.tangem.tap.proxy.redux.DaggerGraphState import org.rekotlin.Middleware @@ -17,7 +15,6 @@ import org.rekotlin.StateType data class AppState( val globalState: GlobalState = GlobalState(), val detailsState: DetailsState = DetailsState(), - val welcomeState: WelcomeState = WelcomeState(), val daggerGraphState: DaggerGraphState = DaggerGraphState(), ) : StateType { @@ -28,7 +25,6 @@ data class AppState( GlobalMiddleware.handler, DetailsMiddleware().detailsMiddleware, BackupMiddleware().backupMiddleware, - WelcomeMiddleware().middleware, LockUserWalletsTimerMiddleware().middleware, AccessCodeRequestPolicyMiddleware().middleware, DaggerGraphMiddleware.daggerGraphMiddleware, diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index c847cb4a50..aee323386c 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -27,8 +27,6 @@ internal object LegacyMiddleware { { action -> when (action) { is LegacyAction.PrepareDetailsScreen -> { - val walletsRepository = store.inject(DaggerGraphState::walletsRepository) - selectedUserWallet() .distinctUntilChanged { old, new -> if (old is UserWallet.Cold && new is UserWallet.Cold) { @@ -39,9 +37,7 @@ internal object LegacyMiddleware { } } .onEach { selectedUserWallet -> - val initializedAppSettingsStateContent = initializeAppSettingsState( - shouldSaveUserWallets = walletsRepository.shouldSaveUserWalletsSync(), - ) + val initializedAppSettingsStateContent = initializeAppSettingsState() store.dispatchWithMain( DetailsAction.PrepareScreen( scanResponse = (selectedUserWallet as? UserWallet.Cold)?.scanResponse, @@ -60,24 +56,15 @@ internal object LegacyMiddleware { } private fun selectedUserWallet(): Flow { - val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles) - return if (hotWalletFeatureToggles.isHotWalletEnabled) { - store.inject(DaggerGraphState::userWalletsListRepository).selectedUserWallet.filterNotNull() - } else { - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - userWalletsListManager.selectedUserWallet - } + return store.inject(DaggerGraphState::userWalletsListRepository).selectedUserWallet.filterNotNull() } /** * LEGACY: We need to initialize [AppSettingsState] async to avoid drawing blocking * previously it was initialized in runBlocking and blocked details screen */ - private suspend fun initializeAppSettingsState(shouldSaveUserWallets: Boolean): AppSettingsState { + private suspend fun initializeAppSettingsState(): AppSettingsState { return AppSettingsState( - isBiometricsAvailable = tangemSdkManager.checkCanUseBiometry(), - saveWallets = shouldSaveUserWallets, - saveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes(), selectedAppCurrency = store.state.globalState.appCurrency, selectedThemeMode = store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT, diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt deleted file mode 100644 index d0fd68250c..0000000000 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.tap.data - -import com.tangem.common.CompletionResult -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import kotlinx.coroutines.flow.Flow - -// FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented -// [REDACTED_JIRA] -internal class RuntimeUserWalletsStore( - private val userWalletsListManager: UserWalletsListManager, -) : UserWalletsStore { - - override val selectedUserWalletOrNull: UserWallet? - get() = userWalletsListManager.selectedUserWalletSync - - override val userWallets: Flow> - get() = userWalletsListManager.userWallets - - override val userWalletsSync: List - get() = userWalletsListManager.userWalletsSync - - override fun getSyncOrNull(key: UserWalletId): UserWallet? { - return userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == key } - } - - override fun getSyncStrict(key: UserWalletId): UserWallet { - return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" } - } - - override suspend fun update( - userWalletId: UserWalletId, - update: suspend (UserWallet) -> UserWallet, - ): CompletionResult { - return userWalletsListManager.update(userWalletId, update) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt index f08b09fb11..8b9dae8020 100644 --- a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt @@ -2,9 +2,6 @@ package com.tangem.tap.di.data import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.features.hotwallet.HotWalletFeatureToggles -import com.tangem.tap.data.RuntimeUserWalletsStore import com.tangem.tap.data.UserWalletsStoreRepositoryProxy import dagger.Module import dagger.Provides @@ -18,15 +15,7 @@ internal object UserWalletsStoreModule { @Provides @Singleton - fun provideUserWalletsStore( - userWalletsListManager: UserWalletsListManager, - userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, - ): UserWalletsStore { - return if (hotWalletFeatureToggles.isHotWalletEnabled) { - UserWalletsStoreRepositoryProxy(userWalletsListRepository) - } else { - RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager) - } + fun provideUserWalletsStore(userWalletsListRepository: UserWalletsListRepository): UserWalletsStore { + return UserWalletsStoreRepositoryProxy(userWalletsListRepository) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt index 8c0d2628db..7f99ec03db 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardDomainModule.kt @@ -9,9 +9,7 @@ import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.derivations.DerivationsRepository -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.* -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase import com.tangem.tap.domain.card.DefaultResetCardUseCase @@ -42,16 +40,8 @@ internal object CardDomainModule { } @Provides - fun provideIsNeedToBackupUseCase( - userWalletsListManager: UserWalletsListManager, - userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, - ): IsNeedToBackupUseCase { - return IsNeedToBackupUseCase( - userWalletsListManager = userWalletsListManager, - userWalletsListRepository = userWalletsListRepository, - useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, - ) + fun provideIsNeedToBackupUseCase(userWalletsListRepository: UserWalletsListRepository): IsNeedToBackupUseCase { + return IsNeedToBackupUseCase(userWalletsListRepository = userWalletsListRepository) } @Provides diff --git a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt index b84ebc02b6..db3b9aa4e2 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/CardLegacyDomainModule.kt @@ -3,9 +3,7 @@ package com.tangem.tap.di.domain import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor import com.tangem.tap.domain.scanCard.LegacyScanProcessor @@ -34,14 +32,8 @@ internal object CardLegacyDomainModule { @Provides @Singleton fun providesWalletNameGenerateUseCase( - userWalletsListManager: UserWalletsListManager, userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, ): GenerateWalletNameUseCase { - return GenerateWalletNameUseCase( - userWalletsListManager = userWalletsListManager, - userWalletsListRepository = userWalletsListRepository, - useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, - ) + return GenerateWalletNameUseCase(userWalletsListRepository = userWalletsListRepository) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index e56953ffdc..d96861be6a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -13,8 +13,6 @@ import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.derivations.DerivationsRepository -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -107,15 +105,11 @@ object MarketsDomainModule { @Provides @Singleton fun provideFilterNetworksUseCase( - userWalletsListManager: UserWalletsListManager, userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, excludedBlockchains: ExcludedBlockchains, ): FilterAvailableNetworksForWalletUseCase { return FilterAvailableNetworksForWalletUseCase( - userWalletsListManager = userWalletsListManager, userWalletsListRepository = userWalletsListRepository, - shouldUseNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, excludedBlockchains = excludedBlockchains, ) } 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 52533057f0..1b3b29d84b 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 @@ -12,7 +12,6 @@ import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.wallets.hot.HotWalletAccessor -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* @@ -21,7 +20,6 @@ import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.operations.attestation.CardArtworksProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -37,119 +35,69 @@ internal object WalletsDomainModule { @Provides fun providesUserWalletsSyncDelegate( - userWalletsListManager: UserWalletsListManager, userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, - dispatchers: CoroutineDispatcherProvider, ): UserWalletsSyncDelegate { - return DefaultUserWalletsSyncDelegate( - userWalletsListManager = userWalletsListManager, - userWalletsListRepository = userWalletsListRepository, - useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, - dispatchers = dispatchers, - ) + return DefaultUserWalletsSyncDelegate(userWalletsListRepository = userWalletsListRepository) } @Provides @Singleton - fun providesGetWalletsUseCase( - userWalletsListManager: UserWalletsListManager, - userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, - ): GetWalletsUseCase { - return GetWalletsUseCase( - userWalletsListManager = userWalletsListManager, - userWalletsListRepository = userWalletsListRepository, - useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, - ) + fun providesGetWalletsUseCase(userWalletsListRepository: UserWalletsListRepository): GetWalletsUseCase { + return GetWalletsUseCase(userWalletsListRepository = userWalletsListRepository) } @Provides @Singleton fun providesWalletNameMigrationUseCase( - userWalletsListManager: UserWalletsListManager, walletNamesMigrationRepository: WalletNamesMigrationRepository, userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, ): WalletNameMigrationUseCase { return WalletNameMigrationUseCase( - userWalletsListManager = userWalletsListManager, walletNamesMigrationRepository = walletNamesMigrationRepository, userWalletsListRepository = userWalletsListRepository, - useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, ) } @Provides @Singleton - fun providesGetUserWalletUseCase( - userWalletsListManager: UserWalletsListManager, - userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, - ): GetUserWalletUseCase { - return GetUserWalletUseCase( - userWalletsListManager = userWalletsListManager, - userWalletsListRepository = userWalletsListRepository, - useNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, - ) + fun providesGetUserWalletUseCase(userWalletsListRepository: UserWalletsListRepository): GetUserWalletUseCase { + return GetUserWalletUseCase(userWalletsListRepository = userWalletsListRepository) } @Provides @Singleton fun providesGetSelectedWalletSyncUseCase( - userWalletsListManager: UserWalletsListManager, userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, ): GetSelectedWalletSyncUseCase { - return GetSelectedWalletSyncUseCase( - userWalletsListManager = userWalletsListManager, - userWalletsListRepository = userWalletsListRepository, - useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, - ) + return GetSelectedWalletSyncUseCase(userWalletsListRepository = userWalletsListRepository) } @Provides @Singleton fun providesGetSelectedWalletUseCase( - userWalletsListManager: UserWalletsListManager, userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, ): GetSelectedWalletUseCase { - return GetSelectedWalletUseCase( - userWalletsListManager = userWalletsListManager, - userWalletsListRepository = userWalletsListRepository, - useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, - ) + return GetSelectedWalletUseCase(userWalletsListRepository = userWalletsListRepository) } @Provides @Singleton fun providesSaveWalletUseCase( - userWalletsListManager: UserWalletsListManager, userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, walletsRepository: WalletsRepository, ): SaveWalletUseCase { return SaveWalletUseCase( - userWalletsListManager = userWalletsListManager, userWalletsListRepository = userWalletsListRepository, walletsRepository = walletsRepository, - useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, ) } @Provides @Singleton fun providesIsWalletAlreadySavedUseCase( - userWalletsListManager: UserWalletsListManager, userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, ): IsWalletAlreadySavedUseCase { - return IsWalletAlreadySavedUseCase( - userWalletsListManager = userWalletsListManager, - userWalletsListRepository = userWalletsListRepository, - useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, - ) + return IsWalletAlreadySavedUseCase(userWalletsListRepository = userWalletsListRepository) } @Provides @@ -164,12 +112,6 @@ internal object WalletsDomainModule { return GetExploreUrlUseCase(walletsManagersFacade = walletsManagersFacade) } - @Provides - @Singleton - fun providesUnlockWalletsUseCase(userWalletsListManager: UserWalletsListManager): UnlockWalletsUseCase { - return UnlockWalletsUseCase(userWalletsListManager = userWalletsListManager) - } - @Provides @Singleton fun providesUnlockWalletUseCase( @@ -197,31 +139,19 @@ internal object WalletsDomainModule { @Provides @Singleton fun providesSelectWalletUseCase( - userWalletsListManager: UserWalletsListManager, userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, reduxStateHolder: ReduxStateHolder, ): SelectWalletUseCase { return SelectWalletUseCase( - userWalletsListManager = userWalletsListManager, userWalletsListRepository = userWalletsListRepository, - useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, reduxStateHolder = reduxStateHolder, ) } @Provides @Singleton - fun providesUpdateWalletUseCase( - userWalletsListManager: UserWalletsListManager, - userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, - ): UpdateWalletUseCase { - return UpdateWalletUseCase( - userWalletsListManager = userWalletsListManager, - userWalletsListRepository = userWalletsListRepository, - useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, - ) + fun providesUpdateWalletUseCase(userWalletsListRepository: UserWalletsListRepository): UpdateWalletUseCase { + return UpdateWalletUseCase(userWalletsListRepository = userWalletsListRepository) } @Provides @@ -238,44 +168,14 @@ internal object WalletsDomainModule { @Provides @Singleton - fun providesGetWalletsSyncUseCase( - userWalletsListManager: UserWalletsListManager, - userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, - ): GetWalletNamesUseCase { - return GetWalletNamesUseCase( - userWalletsListManager = userWalletsListManager, - userWalletsListRepository = userWalletsListRepository, - useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, - ) + fun providesGetWalletsSyncUseCase(userWalletsListRepository: UserWalletsListRepository): GetWalletNamesUseCase { + return GetWalletNamesUseCase(userWalletsListRepository = userWalletsListRepository) } @Provides @Singleton - fun providesDeleteWalletUseCase( - userWalletsListManager: UserWalletsListManager, - userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, - ): DeleteWalletUseCase { - return DeleteWalletUseCase( - userWalletsListManager = userWalletsListManager, - userWalletsListRepository = userWalletsListRepository, - useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, - ) - } - - @Provides - @Singleton - fun providesShouldSaveUserWalletsSyncUseCase( - walletsRepository: WalletsRepository, - ): ShouldSaveUserWalletsSyncUseCase { - return ShouldSaveUserWalletsSyncUseCase(walletsRepository = walletsRepository) - } - - @Provides - @Singleton - fun providesShouldSaveUserWalletsUseCase(walletsRepository: WalletsRepository): ShouldSaveUserWalletsUseCase { - return ShouldSaveUserWalletsUseCase(walletsRepository = walletsRepository) + fun providesDeleteWalletUseCase(userWalletsListRepository: UserWalletsListRepository): DeleteWalletUseCase { + return DeleteWalletUseCase(userWalletsListRepository = userWalletsListRepository) } @Provides @@ -347,15 +247,9 @@ internal object WalletsDomainModule { @Provides @Singleton fun providesGetSavedWalletChangesIdUseCase( - userWalletsListManager: UserWalletsListManager, userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, ): GetSavedWalletsCountUseCase { - return GetSavedWalletsCountUseCase( - userWalletsListManager = userWalletsListManager, - userWalletsListRepository = userWalletsListRepository, - useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, - ) + return GetSavedWalletsCountUseCase(userWalletsListRepository = userWalletsListRepository) } @Provides @@ -479,15 +373,11 @@ internal object WalletsDomainModule { @Provides @Singleton fun provideGetWalletsForAutomaticallyPushEnablingUseCase( - userWalletsListManager: UserWalletsListManager, userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, dispatcherProvider: CoroutineDispatcherProvider, ): GetWalletsForAutomaticallyPushEnablingUseCase { return GetWalletsForAutomaticallyPushEnablingUseCase( - userWalletsListManager = userWalletsListManager, userWalletsListRepository = userWalletsListRepository, - shouldUseNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, dispatchers = dispatcherProvider, ) } 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/UserWalletsListManagerModule.kt index 1c3cfc1d98..3571b8a63c 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/UserWalletsListManagerModule.kt @@ -16,19 +16,14 @@ import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaCardActivationStatus import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository import com.tangem.domain.wallets.hot.HotWalletPasswordRequester -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.hot.sdk.TangemHotSdk import com.tangem.sdk.storage.AndroidSecureStorage import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.sdk.storage.createEncryptedSharedPreferences -import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager -import com.tangem.tap.domain.userWalletList.implementation.GeneralUserWalletsListManager -import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager import com.tangem.tap.domain.userWalletList.repository.DefaultUserWalletsListRepository import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager import com.tangem.tap.domain.userWalletList.repository.UserWalletEncryptionKeysRepository import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator -import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository @@ -47,77 +42,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object UserWalletsListManagerModule { - @Provides - @Singleton - @Deprecated("Use UserWalletsListRepository instead") - fun provideGeneralUserWalletsListManager( - @ApplicationContext applicationContext: Context, - appPreferencesStore: AppPreferencesStore, - dispatchers: CoroutineDispatcherProvider, - analyticsEventHandler: AnalyticsEventHandler, - ): UserWalletsListManager { - return GeneralUserWalletsListManager( - runtimeUserWalletsListManager = RuntimeUserWalletsListManager(), - biometricUserWalletsListManager = createBiometricUserWalletsListManager( - applicationContext = applicationContext, - analyticsEventHandler = analyticsEventHandler, - dispatchers = dispatchers, - ), - appPreferencesStore = appPreferencesStore, - dispatchers = dispatchers, - ) - } - - @Deprecated("Use UserWalletsListRepository instead") - private fun createBiometricUserWalletsListManager( - applicationContext: Context, - analyticsEventHandler: AnalyticsEventHandler, - dispatchers: CoroutineDispatcherProvider, - ): UserWalletsListManager { - val moshi = buildMoshi() - val secureStorage = buildSecureStorage(applicationContext = applicationContext) - - val authenticatedStorage = AuthenticatedStorage( - secureStorage = UserWalletsKeysStoreDecorator( - featureStorage = secureStorage, - cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage }, - ), - keystoreManager = DelegatedKeystoreManager( - keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager }, - ), - ) - - val keysRepository = BiometricUserWalletsKeysRepository( - moshi = moshi, - secureStorage = secureStorage, - authenticatedStorage = authenticatedStorage, - analyticsEventHandler = analyticsEventHandler, - ) - - val publicInformationRepository = DefaultUserWalletsPublicInformationRepository( - moshi = moshi, - secureStorage = secureStorage, - ) - - val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository( - moshi = moshi, - secureStorage = secureStorage, - ) - - val selectedUserWalletRepository = DefaultSelectedUserWalletRepository( - secureStorage = secureStorage, - dispatchers = dispatchers, - ) - - return BiometricUserWalletsListManager( - keysRepository = keysRepository, - publicInformationRepository = publicInformationRepository, - sensitiveInformationRepository = sensitiveInformationRepository, - selectedUserWalletRepository = selectedUserWalletRepository, - dispatcherProvider = dispatchers, - ) - } - @Provides @Singleton fun provideUserWalletsListRepository( @@ -183,7 +107,7 @@ internal object UserWalletsListManagerModule { ) } - fun buildMoshi(): Moshi { + private fun buildMoshi(): Moshi { return Moshi.Builder() .add(WalletDerivedKeysMapAdapter()) .add(ScanResponseDerivedKeysMapAdapter()) @@ -200,7 +124,7 @@ internal object UserWalletsListManagerModule { .build() } - fun buildSecureStorage(@ApplicationContext applicationContext: Context): SecureStorage { + private fun buildSecureStorage(@ApplicationContext applicationContext: Context): SecureStorage { return AndroidSecureStorage( preferences = SecureStorage.createEncryptedSharedPreferences( context = applicationContext, diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt deleted file mode 100644 index 8183415675..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ /dev/null @@ -1,434 +0,0 @@ -package com.tangem.tap.domain.userWalletList.implementation - -import com.tangem.common.* -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.wallets.legacy.UserWalletsListError -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType -import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey -import com.tangem.tap.domain.userWalletList.repository.SelectedUserWalletRepository -import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository -import com.tangem.tap.domain.userWalletList.repository.UserWalletsPublicInformationRepository -import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository -import com.tangem.tap.domain.userWalletList.utils.encryptionKey -import com.tangem.tap.domain.userWalletList.utils.lockAll -import com.tangem.tap.domain.userWalletList.utils.toUserWallets -import com.tangem.tap.domain.userWalletList.utils.updateWith -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import timber.log.Timber - -@Suppress("LargeClass") -@OptIn(ExperimentalCoroutinesApi::class) -internal class BiometricUserWalletsListManager( - private val keysRepository: UserWalletsKeysRepository, - private val publicInformationRepository: UserWalletsPublicInformationRepository, - private val sensitiveInformationRepository: UserWalletsSensitiveInformationRepository, - private val selectedUserWalletRepository: SelectedUserWalletRepository, - private val dispatcherProvider: CoroutineDispatcherProvider, -) : UserWalletsListManager.Lockable { - private val state = MutableStateFlow(State()) - - private var hasSavedWallets: Boolean? = null - private val savedWalletMutex = Mutex() - - override val isLockable: Boolean = true - - override val userWallets: Flow> - get() = state - .mapLatest { it.userWallets } - .distinctUntilChanged() - - override val userWalletsSync: List - get() = state.value.userWallets - - @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") - override val selectedUserWallet: Flow - get() = state - .mapLatest { state -> - findSelectedUserWallet(state.userWallets) - } - .filterNotNull() - .distinctUntilChanged() - - @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") - override val selectedUserWalletSync: UserWallet? - get() = findSelectedUserWallet() - - override val lockedState: Flow - get() = state - .mapLatest { it.isLocked } - .distinctUntilChanged() - - override val isLocked: Boolean - get() = state.value.isLocked - - override val hasUserWallets: Boolean - get() { - return runBlocking { - // workaround to avoid calling hasSavedEncryptionKeys many times because of performance - savedWalletMutex.withLock { - Timber.i("Checking if user has saved wallets") - val hasSavedWalletsLocal = hasSavedWallets - if (hasSavedWalletsLocal == null || !hasSavedWalletsLocal) { - val hasKeys = keysRepository.hasSavedEncryptionKeys() - hasSavedWallets = hasKeys - hasKeys - } else { - Timber.i("User has saved wallets (from cache)") - true - } - } - } - } - - override val walletsCount: Int - get() = state.value.userWallets.size - - override val savedWalletsCount: Flow - get() = state - .mapLatest { walletsCount } - .distinctUntilChanged() - - override suspend fun unlock(type: UnlockType): CompletionResult { - return withContext(dispatcherProvider.io) { - unlockAndSetSelectedUserWallet(type) - .mapFailure { error -> - Timber.e(error, "Unable to unlock user wallets") - if (error is UserWalletsListError) { - error - } else { - UserWalletsListError.UnableToUnlockUserWallets(error) - } - } - .map { selectedUserWallet -> - if (selectedUserWallet == null || selectedUserWallet.isLocked) { - Timber.e("Unable to find selected user wallet") - throw UserWalletsListError.NoUserWalletSelected - } else { - selectedUserWallet - } - } - } - } - - override fun lock() { - state.update { prevState -> - prevState.copy( - encryptionKeys = emptyList(), - userWallets = prevState.userWallets.lockAll(), - isLocked = true, - ) - } - } - - override suspend fun select(userWalletId: UserWalletId): CompletionResult = catching { - if (state.value.selectedUserWalletId == userWalletId) { - return@catching requireNotNull(findSelectedUserWallet()) { - "Wallet is not found" - } - } - - selectedUserWalletRepository.set(userWalletId) - - val newState = state.updateAndGet { prevState -> - prevState.copy( - selectedUserWalletId = userWalletId, - ) - } - - newState.userWallets.first { it.walletId == userWalletId } - } - - override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult { - return withContext(dispatcherProvider.io) { - if (canOverride) { - saveInternal(userWallet, changeSelectedUserWallet = true, canOverridePublicInfo = false) - } else { - val isWalletSaved = state.value.userWallets - .any { - it.walletId == userWallet.walletId - } - - if (isWalletSaved) { - CompletionResult.Failure(UserWalletsListError.WalletAlreadySaved) - } else { - saveInternal(userWallet, changeSelectedUserWallet = true, canOverridePublicInfo = false) - } - } - } - } - - override suspend fun update( - userWalletId: UserWalletId, - update: suspend (UserWallet) -> UserWallet, - ): CompletionResult { - return withContext(dispatcherProvider.io) { - get(userWalletId) - .map { storedUserWallet -> - update(storedUserWallet) - } - .flatMap { updatedUserWallet -> - saveInternal(updatedUserWallet, changeSelectedUserWallet = false, canOverridePublicInfo = true) - } - .flatMap { - get(userWalletId) - } - } - } - - override suspend fun delete(userWalletIds: List): CompletionResult { - val idsToRemove = state.value.userWallets - .takeIf { it.isNotEmpty() } - ?.filter { it.walletId in userWalletIds } - ?.map { it.walletId } - - if (idsToRemove.isNullOrEmpty()) { - return CompletionResult.Success(Unit) - } - - if (idsToRemove.size == state.value.userWallets.size) { - return clear() - } - - return withContext(dispatcherProvider.io) { - sensitiveInformationRepository.delete(idsToRemove) - .flatMap { publicInformationRepository.delete(idsToRemove) } - .map { keysRepository.delete(idsToRemove) } - .map { - state.update { prevState -> - val remainingWallets = prevState.userWallets.filter { it.walletId !in idsToRemove } - - val isSelectedWalletDeleted = prevState.selectedUserWalletId in idsToRemove - val newSelectedUserWallet = findOrSetSelectedWallet( - prevSelectedWalletId = prevState.selectedUserWalletId, - prevSelectedWalletIndex = prevState.userWallets.indexOfFirst { - it.walletId == prevState.selectedUserWalletId - }, - userWallets = remainingWallets, - ignorePrevSelectedWallet = isSelectedWalletDeleted, - ) - - prevState.copy( - encryptionKeys = prevState.encryptionKeys.filter { it.walletId !in idsToRemove }, - userWallets = remainingWallets, - isLocked = remainingWallets.any { it.isLocked }, - selectedUserWalletId = newSelectedUserWallet?.walletId, - ) - } - } - } - } - - override suspend fun clear(): CompletionResult { - savedWalletMutex.withLock { - hasSavedWallets = null - } - return withContext(dispatcherProvider.io) { - sensitiveInformationRepository.clear() - .flatMap { publicInformationRepository.clear() } - .map { - keysRepository.clear() - selectedUserWalletRepository.set(null) - state.value = State() - } - } - } - - override suspend fun get(userWalletId: UserWalletId): CompletionResult { - return catching { - state.value.userWallets.first { it.walletId == userWalletId } - } - } - - private suspend fun saveInternal( - userWallet: UserWallet, - changeSelectedUserWallet: Boolean, - canOverridePublicInfo: Boolean, - ): CompletionResult { - val encryptionKey = userWallet.encryptionKey - ?.let { UserWalletEncryptionKey(userWallet.walletId, it) } - ?: return CompletionResult.Success(Unit) // No encryption key, no need to save - - return keysRepository.save(encryptionKey) - .flatMap { sensitiveInformationRepository.save(userWallet, encryptionKey = encryptionKey.encryptionKey) } - .flatMap { publicInformationRepository.save(userWallet, canOverridePublicInfo) } - .flatMap { - loadUserWallets( - encryptionKeys = state.value.encryptionKeys - .plus(encryptionKey) - .distinctBy(UserWalletEncryptionKey::walletId), - ) - } - .doOnSuccess { loadedState -> - if (changeSelectedUserWallet) { - selectedUserWalletRepository.set(userWallet.walletId) - - state.value = loadedState.copy( - selectedUserWalletId = userWallet.walletId, - ) - } else { - state.value = loadedState - } - } - .map { /* Type erasing */ } - } - - private suspend fun unlockAndSetSelectedUserWallet(type: UnlockType): CompletionResult { - return keysRepository.getAll() - .flatMap { encryptionKeys -> - loadUserWallets( - encryptionKeys = state.value.encryptionKeys - .plus(encryptionKeys) - .distinctBy(UserWalletEncryptionKey::walletId), - ) - } - .map { loadedState -> - when (type) { - UnlockType.ALL -> { - if (loadedState.isLocked) { - Timber.e("Some user wallets remain locked") - - state.value = loadedState - - throw UserWalletsListError.NotAllUserWalletsUnlocked - } else { - val prevState = state.value - - val selectedWallet = findOrSetSelectedWallet( - prevSelectedWalletId = prevState.selectedUserWalletId, - userWallets = loadedState.userWallets, - prevSelectedWalletIndex = prevState.userWallets.indexOfFirst { - it.walletId == prevState.selectedUserWalletId - }, - ) - - state.value = loadedState.copy(selectedUserWalletId = selectedWallet?.walletId) - - selectedWallet - } - } - UnlockType.ANY -> { - val prevState = state.value - - val selectedWallet = findOrSetSelectedWallet( - prevSelectedWalletId = state.value.selectedUserWalletId, - prevSelectedWalletIndex = prevState.userWallets.indexOfFirst { - it.walletId == prevState.selectedUserWalletId - }, - userWallets = loadedState.userWallets, - ) - - state.value = loadedState.copy(selectedUserWalletId = selectedWallet?.walletId) - - selectedWallet - } - UnlockType.ALL_WITHOUT_SELECT -> { - state.value = loadedState - - findSelectedUserWallet() - } - } - } - } - - private suspend fun loadUserWallets(encryptionKeys: List): CompletionResult { - return publicInformationRepository.getAll() - .map { it.toUserWallets() } - .flatMap { userWallets -> - sensitiveInformationRepository.getAll(encryptionKeys) - .map { walletIdToSensitiveInformation -> - userWallets.updateWith(walletIdToSensitiveInformation) - } - } - .map { userWallets -> - val prevState = state.value - - if (userWallets.isNotEmpty()) { - val newUserWallets = (userWallets + prevState.userWallets) - .distinctBy(UserWallet::walletId) - - prevState.copy( - userWallets = newUserWallets, - encryptionKeys = encryptionKeys, - isLocked = newUserWallets.any(UserWallet::isLocked), - ) - } else { - prevState - } - } - } - - private suspend fun findOrSetSelectedWallet( - prevSelectedWalletId: UserWalletId?, - prevSelectedWalletIndex: Int, - userWallets: List, - ignorePrevSelectedWallet: Boolean = false, - ): UserWallet? { - var possibleSelectedUserWallet: UserWallet? = null - - if (!ignorePrevSelectedWallet) { - val selectedWalletId = prevSelectedWalletId ?: selectedUserWalletRepository.get() - possibleSelectedUserWallet = findSelectedUserWallet(userWallets, selectedWalletId) - } - - if (possibleSelectedUserWallet == null || possibleSelectedUserWallet.isLocked) { - possibleSelectedUserWallet = - userWallets.findAvailableUserWallet(prevSelectedIndex = prevSelectedWalletIndex) - } - - selectedUserWalletRepository.set(possibleSelectedUserWallet?.walletId) - - return possibleSelectedUserWallet - } - - /** - * Find the nearest available wallet that can be selected - * - * Example: - * Number with *n* is previous selected wallet with index [prevSelectedIndex]. - * - * 1. [*1*, 2, 3, 4] => delete 1 => [2, 3, 4] => find and select => [*2*, 3, 4] - * 2. [1, *2*, 3, 4] => delete 2 => [1, 3, 4] => find and select => [1, *3*, 4] - * 3. [1, 2, *3*, 4] => delete 3 => [1, 2, 4] => find and select => [1, 2, *4*] - * 4. [1, 2, 3, *4*] => delete 4 => [1, 2, 3] => find and select => [1, 2, *3*] - * - * @receiver list of user wallets without deleted wallet - */ - private fun List.findAvailableUserWallet(prevSelectedIndex: Int): UserWallet? { - if (prevSelectedIndex == 0) return firstOrNull { !it.isLocked } ?: firstOrNull() - - if (prevSelectedIndex in indices && !this[prevSelectedIndex].isLocked) return this[prevSelectedIndex] - - for (offset in 1..size) { - val rightIndex = prevSelectedIndex + offset - if (rightIndex in indices && !this[rightIndex].isLocked) return this[rightIndex] - - val leftIndex = prevSelectedIndex - offset - if (leftIndex in indices && !this[leftIndex].isLocked) return this[leftIndex] - } - - return lastOrNull() - } - - private fun findSelectedUserWallet( - userWallets: List = state.value.userWallets, - selectedUserWalletId: UserWalletId? = state.value.selectedUserWalletId, - ): UserWallet? { - return userWallets.firstOrNull { it.walletId == selectedUserWalletId } - } - - private data class State( - val encryptionKeys: List = emptyList(), - val userWallets: List = emptyList(), - val selectedUserWalletId: UserWalletId? = null, - val isLocked: Boolean = true, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt deleted file mode 100644 index f38d94f5c8..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt +++ /dev/null @@ -1,203 +0,0 @@ -package com.tangem.tap.domain.userWalletList.implementation - -import com.tangem.common.CompletionResult -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.get -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* -import timber.log.Timber - -/** - * General implementation of [UserWalletsListManager] that helps to switch between Runtime and Biometric - * implementations. - * - * @property runtimeUserWalletsListManager runtime user wallets list manager - * @property biometricUserWalletsListManager biometric user wallets list manager - * @property appPreferencesStore app preferences store - * @property dispatchers coroutine dispatcher provider - * -[REDACTED_AUTHOR] - */ -@OptIn(ExperimentalCoroutinesApi::class) -internal class GeneralUserWalletsListManager( - private val runtimeUserWalletsListManager: UserWalletsListManager, - private val biometricUserWalletsListManager: UserWalletsListManager, - private val appPreferencesStore: AppPreferencesStore, - private val dispatchers: CoroutineDispatcherProvider, -) : UserWalletsListManager.Lockable { - - private val applicationScope = CoroutineScope(dispatchers.io) - private val implementation: MutableStateFlow = MutableStateFlow(value = null) - - private val requireImplementation: UserWalletsListManager - get() = requireNotNull(implementation.value) { - "UserWalletsListManager is not initialized" - } - - init { - subscribeOnCurrentManager() - } - - override val isLockable: Boolean - get() = requireImplementation.isLockable - - override val userWallets: Flow> - get() = implementation - .transformLatest { impl -> - if (impl != null) { - emitAll(impl.userWallets) - } - } - // To avoid returning empty flow to subscriber while implementation and userWallets are null - // Flow is called first time when implementation is null and then when its assigned with implementation - // that may have not user wallets (null or empty). - // As a result subscription occurs on empty flow, than will not change if user wallets are available - .filter { requireImplementation.hasUserWallets } - - override val savedWalletsCount: Flow - get() = implementation - .transformLatest { impl -> - if (impl != null) { - emitAll(impl.savedWalletsCount) - } - } - - override val userWalletsSync: List - get() = requireImplementation.userWalletsSync - - override val selectedUserWallet: Flow - get() = implementation - .transformLatest { impl -> - if (impl != null) { - emitAll(impl.selectedUserWallet) - } - } - // To avoid returning empty flow to subscriber while implementation and userWallets are null - // Flow is called first time when implementation is null and then when its assigned with implementation - // that may have not user wallets (null or empty). - // As a result subscription occurs on empty flow, than will not change if user wallets are available - .filter { requireImplementation.hasUserWallets } - - override val selectedUserWalletSync: UserWallet? - get() = requireImplementation.selectedUserWalletSync - - override val hasUserWallets: Boolean - get() = requireImplementation.hasUserWallets - - override val walletsCount: Int - get() = requireImplementation.walletsCount - - override val lockedState: Flow - get() = implementation.transformLatest { impl -> - if (impl == null) return@transformLatest - - if (impl is UserWalletsListManager.Lockable) { - emitAll(impl.lockedState) - } else { - error("RuntimeUserWalletsListManager is not lockable") - } - } - - override val isLocked: Boolean - get() { - val impl = requireImplementation - - return if (impl is UserWalletsListManager.Lockable) { - impl.isLocked - } else { - error("RuntimeUserWalletsListManager is not lockable") - } - } - - override suspend fun select(userWalletId: UserWalletId): CompletionResult { - return requireImplementation.select(userWalletId) - } - - override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult { - return requireImplementation.save(userWallet, canOverride) - } - - override suspend fun update( - userWalletId: UserWalletId, - update: suspend (UserWallet) -> UserWallet, - ): CompletionResult { - return requireImplementation.update(userWalletId, update) - } - - override suspend fun delete(userWalletIds: List): CompletionResult { - return requireImplementation.delete(userWalletIds) - } - - override suspend fun clear(): CompletionResult { - return requireImplementation.clear() - } - - override suspend fun get(userWalletId: UserWalletId): CompletionResult { - return requireImplementation.get(userWalletId) - } - - override suspend fun unlock(type: UserWalletsListManager.Lockable.UnlockType): CompletionResult { - val implementation = requireImplementation - - return if (implementation is UserWalletsListManager.Lockable) { - implementation.unlock(type) - } else { - error("RuntimeUserWalletsListManager is not lockable") - } - } - - override fun lock() { - val implementation = requireImplementation - - return if (implementation is UserWalletsListManager.Lockable) { - implementation.lock() - } else { - error("RuntimeUserWalletsListManager is not lockable") - } - } - - private fun subscribeOnCurrentManager() { - appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) - .distinctUntilChanged() - .onEach { shouldSaveUserWallets -> - val possibleManager = if (shouldSaveUserWallets) { - biometricUserWalletsListManager - } else { - runtimeUserWalletsListManager - } - - if (possibleManager == implementation.value) { - Timber.e("Switch to the same manager ${possibleManager::class.simpleName.orEmpty()}") - } - - Timber.i("Switch to ${possibleManager::class.simpleName.orEmpty()}") - - val previousManager = implementation.value - implementation.value = copySelectedUserWallet( - sourceManager = previousManager, - destinationManager = possibleManager, - ) - - previousManager?.clear() - } - .flowOn(dispatchers.io) - .launchIn(applicationScope) - } - - private suspend fun copySelectedUserWallet( - sourceManager: UserWalletsListManager?, - destinationManager: UserWalletsListManager, - ): UserWalletsListManager { - sourceManager?.selectedUserWalletSync?.let { selectedWallet -> - destinationManager.save(selectedWallet, canOverride = true) - } - - return destinationManager - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt deleted file mode 100644 index 8ee8847ffa..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/RuntimeUserWalletsListManager.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.tangem.tap.domain.userWalletList.implementation - -import com.tangem.common.CompletionResult -import com.tangem.common.catching -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.legacy.UserWalletsListError -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* - -@OptIn(ExperimentalCoroutinesApi::class) -internal class RuntimeUserWalletsListManager : UserWalletsListManager { - private val state = MutableStateFlow(State()) - - override val isLockable: Boolean = false - - override val userWallets: Flow> - get() = state - .mapLatest { listOfNotNull(it.userWallet) } - .distinctUntilChanged() - - override val selectedUserWallet: Flow - get() = state - .mapLatest { it.userWallet } - .filterNotNull() - .distinctUntilChanged() - - override val userWalletsSync: List - get() = listOfNotNull(state.value.userWallet) - - override val selectedUserWalletSync: UserWallet? - get() = state.value.userWallet - - override val hasUserWallets: Boolean - get() = state.value.userWallet != null - - /** - * only 1 wallet stored in runtime implementation - */ - override val walletsCount: Int - get() = if (hasUserWallets) 1 else 0 - - override val savedWalletsCount: Flow - get() = state - .mapLatest { walletsCount } - .distinctUntilChanged() - - override suspend fun select(userWalletId: UserWalletId): CompletionResult = catching { - state.value.userWallet - ?.takeIf { it.walletId == userWalletId } - ?: walletNotFound() - } - - override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult { - return if (canOverride) { - saveInternal(userWallet) - } else { - val isWalletSaved = state.value.userWallet?.walletId == userWallet.walletId - - if (isWalletSaved) { - CompletionResult.Failure(UserWalletsListError.WalletAlreadySaved) - } else { - saveInternal(userWallet) - } - } - } - - override suspend fun update( - userWalletId: UserWalletId, - update: suspend (UserWallet) -> UserWallet, - ): CompletionResult = catching { - val wallet = state.value.userWallet - ?.takeIf { it.walletId == userWalletId } - ?: walletNotFound() - - requireNotNull( - state.updateAndGet { prevState -> - prevState.copy( - userWallet = update(wallet), - ) - }.userWallet, - ) { "User wallet is null after update" } - } - - override suspend fun delete(userWalletIds: List): CompletionResult = clear() - - override suspend fun clear(): CompletionResult = catching { - state.update { prevState -> - prevState.copy( - userWallet = null, - ) - } - } - - override suspend fun get(userWalletId: UserWalletId): CompletionResult = catching { - state.value.userWallet ?: walletNotFound() - } - - private fun saveInternal(userWallet: UserWallet): CompletionResult = catching { - state.update { prevState -> - prevState.copy( - userWallet = userWallet, - ) - } - } - - private fun walletNotFound(): Nothing { - throw NoSuchElementException("User wallet not found") - } - - private data class State( - val userWallet: UserWallet? = null, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricFailReasonConverter.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricFailReasonConverter.kt deleted file mode 100644 index 89013b048e..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricFailReasonConverter.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.tap.domain.userWalletList.repository.implementation - -import com.tangem.common.core.TangemError -import com.tangem.common.core.TangemSdkError -import com.tangem.core.analytics.models.Basic -import com.tangem.domain.wallets.legacy.UserWalletsListError -import com.tangem.utils.converter.Converter - -object BiometricFailReasonConverter : Converter { - - override fun convert(value: TangemError): Basic.BiometryFailed.BiometricFailReason { - // 1. Try handle TangemSdkError cases first if error has not been mapped - when (value) { - is TangemSdkError.AuthenticationCanceled -> - return Basic.BiometryFailed.BiometricFailReason.AuthenticationCancelled - is TangemSdkError.AuthenticationAlreadyInProgress -> - return Basic.BiometryFailed.BiometricFailReason.AuthenticationAlreadyInProgress - } - - // 2. For other errors, check if they are of type UserWalletsListError - if (value !is UserWalletsListError) { - return Basic.BiometryFailed.BiometricFailReason.Other(value.customMessage) - } - // 3. Map UserWalletsListError to BiometricFailReason - return when (value) { - UserWalletsListError.AllKeysInvalidated -> - Basic.BiometryFailed.BiometricFailReason.AllKeysInvalidated - UserWalletsListError.BiometricsAuthenticationDisabled -> - Basic.BiometryFailed.BiometricFailReason.BiometricsAuthenticationDisabled - is UserWalletsListError.BiometricsAuthenticationLockout -> - if (value.isPermanent) { - Basic.BiometryFailed.BiometricFailReason.AuthenticationLockoutPermanent - } else { - Basic.BiometryFailed.BiometricFailReason.AuthenticationLockout - } - else -> Basic.BiometryFailed.BiometricFailReason.Other(value.customMessage) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt deleted file mode 100644 index 6f82167edd..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt +++ /dev/null @@ -1,207 +0,0 @@ -package com.tangem.tap.domain.userWalletList.repository.implementation - -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import com.tangem.common.* -import com.tangem.common.authentication.storage.AuthenticatedStorage -import com.tangem.common.core.TangemSdkError -import com.tangem.common.services.secure.SecureStorage -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.legacy.UserWalletsListError -import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey -import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext - -internal class BiometricUserWalletsKeysRepository( - moshi: Moshi, - private val authenticatedStorage: AuthenticatedStorage, - private val secureStorage: SecureStorage, - private val analyticsEventHandler: AnalyticsEventHandler, -) : UserWalletsKeysRepository { - - private val encryptionKeyAdapter: JsonAdapter = moshi.adapter( - UserWalletEncryptionKey::class.java, - ) - private val userWalletsIdsListAdapter: JsonAdapter> = moshi.adapter( - Types.newParameterizedType(List::class.java, UserWalletId::class.java), - ) - - override suspend fun getAll(): CompletionResult> { - return withContext(Dispatchers.IO) { - getAllInternal() - .mapFailure { error -> - val mappedError = when (error) { - is TangemSdkError.AuthenticationLockout -> - UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = false) - is TangemSdkError.AuthenticationPermanentLockout -> - UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = true) - is TangemSdkError.KeystoreInvalidated -> - UserWalletsListError.AllKeysInvalidated - is TangemSdkError.AuthenticationUnavailable -> - UserWalletsListError.BiometricsAuthenticationDisabled - else -> error - } - analyticsEventHandler.send( - Basic.BiometryFailed( - source = AnalyticsParam.ScreensSources.SignIn, - reason = BiometricFailReasonConverter.convert(mappedError), - ), - ) - mappedError - } - } - } - - override suspend fun save(encryptionKey: UserWalletEncryptionKey): CompletionResult { - return withContext(Dispatchers.IO) { - storeEncryptionKey(encryptionKey) - } - } - - override suspend fun delete(userWalletsIds: List) { - return withContext(Dispatchers.IO) { - userWalletsIds.forEach { userWalletId -> - deleteEncryptionKey(userWalletId) - } - - deleteUserWalletsIds(userWalletsIds) - } - } - - override suspend fun clear() { - return withContext(Dispatchers.IO) { - getUserWalletsIds() - .forEach { userWalletId -> - deleteEncryptionKey(userWalletId) - } - - clearUserWalletsIds() - } - } - - override fun hasSavedEncryptionKeys(): Boolean { - return runBlocking { - getUserWalletsIds().isNotEmpty() - } - } - - private suspend fun getAllInternal(): CompletionResult> { - return catching { - val userWalletIds = getUserWalletsIds() - - getEncryptionKeys(userWalletIds) - } - .doOnFailure { error -> - when (error) { - is TangemSdkError.KeystoreInvalidated -> { - getUserWalletsIds().forEach { userWalletId -> - deleteEncryptionKey(userWalletId) - } - } - else -> Unit - } - } - } - - private suspend fun getEncryptionKeys(userWalletsIds: List): List { - val keys = userWalletsIds.map { userWalletId -> - StorageKey.UserWalletEncryptionKey(userWalletId).name - } - - return authenticatedStorage.get(keys).mapNotNull { (_, encodedData) -> - encodedData.decodeToKey() - } - } - - private suspend fun storeEncryptionKey(encryptionKey: UserWalletEncryptionKey): CompletionResult { - return catching { - authenticatedStorage.store( - keyAlias = StorageKey.UserWalletEncryptionKey(encryptionKey.walletId).name, - data = encryptionKey.encode(), - ) - } - .map { storeUserWalletId(encryptionKey.walletId) } - } - - private fun deleteEncryptionKey(userWalletId: UserWalletId) { - authenticatedStorage.delete(StorageKey.UserWalletEncryptionKey(userWalletId).name) - } - - private suspend fun getUserWalletsIds(): List { - return withContext(Dispatchers.IO) { - secureStorage.get(StorageKey.UserWalletIds.name) - .decodeToUserWalletsIds() - } - } - - private suspend fun storeUserWalletId(userWalletId: UserWalletId) { - val userWalletIds = (getUserWalletsIds() + userWalletId).distinct() - - withContext(Dispatchers.IO) { - secureStorage.store(userWalletIds.encode(), StorageKey.UserWalletIds.name) - } - } - - private suspend fun deleteUserWalletsIds(userWalletsIds: List) { - val remainingIds = getUserWalletsIds() - userWalletsIds.toSet() - - withContext(Dispatchers.IO) { - secureStorage.store(remainingIds.encode(), StorageKey.UserWalletIds.name) - } - } - - private suspend fun clearUserWalletsIds() { - withContext(Dispatchers.IO) { - secureStorage.delete(StorageKey.UserWalletIds.name) - } - } - - private suspend fun UserWalletEncryptionKey.encode(): ByteArray { - return withContext(Dispatchers.Default) { - encryptionKeyAdapter.toJson(this@encode) - .encodeToByteArray(throwOnInvalidSequence = true) - } - } - - private suspend fun ByteArray?.decodeToKey(): UserWalletEncryptionKey? { - return withContext(Dispatchers.Default) { - this@decodeToKey - ?.decodeToString(throwOnInvalidSequence = true) - ?.let(encryptionKeyAdapter::fromJson) - } - } - - private suspend fun List.encode(): ByteArray { - return withContext(Dispatchers.Default) { - userWalletsIdsListAdapter.toJson(this@encode) - .encodeToByteArray(throwOnInvalidSequence = true) - } - } - - private suspend fun ByteArray?.decodeToUserWalletsIds(): List { - return withContext(Dispatchers.Default) { - this@decodeToUserWalletsIds - ?.decodeToString(throwOnInvalidSequence = true) - ?.let(userWalletsIdsListAdapter::fromJson) - .orEmpty() - } - } - - private sealed interface StorageKey { - val name: String - - class UserWalletEncryptionKey(userWalletId: UserWalletId) : StorageKey { - override val name: String = "user_wallet_encryption_key_${userWalletId.stringValue}" - } - - object UserWalletIds : StorageKey { - override val name: String = "user_wallets_ids_with_saved_keys" - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt index 2b08bba4a9..c9184f1335 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt @@ -104,8 +104,6 @@ internal fun List.updateWith( } } -internal fun List.lockAll(): List = map(UserWallet::lock) - internal fun UserWallet.lock(): UserWallet = when (this) { is UserWallet.Cold -> { copy( 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 e3e1923c6b..6ecd467e40 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 @@ -3,15 +3,12 @@ package com.tangem.tap.features.details.redux import com.tangem.common.CompletionResult import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess -import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.common.wallets.UserWalletsListRepository.LockMethod -import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings -import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState @@ -43,7 +40,7 @@ class DetailsMiddleware { if (!DemoHelper.tryHandle(stateProvider)) { val detailsState = stateProvider()?.detailsState if (detailsState != null) { - handleAction(detailsState, action) + handleAction(action) } } next(action) @@ -51,9 +48,9 @@ class DetailsMiddleware { } } - private fun handleAction(state: DetailsState, action: Action) { + private fun handleAction(action: Action) { when (action) { - is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, action) + is DetailsAction.AppSettings -> appSettingsMiddleware.handle(action) } } @@ -61,12 +58,10 @@ class DetailsMiddleware { private val checkBiometricsStatusJobHolder = JobHolder() - fun handle(state: DetailsState, action: DetailsAction.AppSettings) { + fun handle(action: DetailsAction.AppSettings) { when (action) { is DetailsAction.AppSettings.SwitchPrivacySetting -> { when (action.setting) { - AppSetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable) - AppSetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable) AppSetting.RequireAccessCode -> toggleRequireAccessCode(enable = action.enable) AppSetting.BiometricAuthentication -> toggleBiometricsAuthentication(enable = action.enable) } @@ -221,118 +216,6 @@ class DetailsMiddleware { } } - private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch { - // Nothing to change - val walletsRepository = store.inject(DaggerGraphState::walletsRepository) - - if (walletsRepository.shouldSaveUserWalletsSync() == enable) { - store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) - return@launch - } - - toggleSaveWallets(state.scanResponse, enable) - .doOnFailure { - store.dispatchWithMain( - DetailsAction.AppSettings.SwitchPrivacySetting.Failure( - prevState = !enable, - setting = AppSetting.SaveWallets, - ), - ) - } - .doOnSuccess { - store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) - } - } - - private suspend fun toggleSaveWallets(scanResponse: ScanResponse?, enable: Boolean): CompletionResult { - return if (enable) { - saveCurrentWallet(scanResponse, enableAccessCodesSaving = false) - } else { - deleteSavedWalletsAndAccessCodes() - } - } - - private fun toggleSaveAccessCodes(state: DetailsState, enable: Boolean) = scope.launch { - val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() - - // Nothing to change - if (shouldSaveAccessCodes == enable) { - store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) - return@launch - } - - toggleSaveAccessCodes(state.scanResponse, state.appSettingsState.saveWallets, enable) - .doOnFailure { - store.dispatchWithMain( - DetailsAction.AppSettings.SwitchPrivacySetting.Failure( - prevState = !enable, - setting = AppSetting.SaveAccessCode, - ), - ) - } - .doOnSuccess { - store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) - } - } - - private suspend fun toggleSaveAccessCodes( - scanResponse: ScanResponse?, - isWalletsSavingEnabled: Boolean, - enable: Boolean, - ): CompletionResult { - return if (enable) { - if (!isWalletsSavingEnabled) { - saveCurrentWallet(scanResponse, enableAccessCodesSaving = true) - } else { - saveAccessCodes(scanResponse) - } - } else { - deleteSavedAccessCodes() - } - } - - private suspend fun saveCurrentWallet( - scanResponse: ScanResponse?, - enableAccessCodesSaving: Boolean, - ): CompletionResult { - store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true) - - return if (enableAccessCodesSaving) { - saveAccessCodes(scanResponse) - } else { - CompletionResult.Success(Unit) - } - .doOnSuccess { - Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.On)) - } - .doOnFailure { error -> - Timber.e(error, "Unable to save user wallet") - } - } - - private suspend fun deleteSavedWalletsAndAccessCodes(): CompletionResult { - Analytics.send(Settings.AppSettings.SaveWalletSwitcherChanged(AnalyticsParam.OnOffState.Off)) - - deleteSavedAccessCodes() - store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false) - - store.dispatchNavigationAction { replaceAll(AppRoute.Home()) } - - return CompletionResult.Success(Unit) - } - - private suspend fun saveAccessCodes(scanResponse: ScanResponse?): CompletionResult { - Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.On)) - - store.inject(DaggerGraphState::settingsRepository).setShouldSaveAccessCodes(value = true) - - store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = scanResponse?.card?.isAccessCodeSet == true, - ) - - return CompletionResult.Success(Unit) - } - private suspend fun deleteSavedAccessCodes(): CompletionResult { return tangemSdkManager.clearSavedUserCodes() .doOnSuccess { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index 54a13d6915..a01428dbfd 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -38,15 +38,6 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail return when (action) { is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy( appSettingsState = when (action.setting) { - AppSetting.SaveWallets -> state.appSettingsState.copy( - isInProgress = true, - saveWallets = action.enable, - ) - AppSetting.SaveAccessCode -> state.appSettingsState.copy( - isInProgress = true, - saveWallets = true, // User can't enable access codes saving without wallets saving - saveAccessCodes = action.enable, - ) AppSetting.RequireAccessCode -> state.appSettingsState.copy( isInProgress = true, requireAccessCode = action.enable, @@ -64,14 +55,6 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail ) is DetailsAction.AppSettings.SwitchPrivacySetting.Failure -> state.copy( appSettingsState = when (action.setting) { - AppSetting.SaveWallets -> state.appSettingsState.copy( - isInProgress = false, - saveWallets = action.prevState, - ) - AppSetting.SaveAccessCode -> state.appSettingsState.copy( - isInProgress = false, - saveAccessCodes = action.prevState, - ) AppSetting.RequireAccessCode -> state.appSettingsState.copy( isInProgress = false, requireAccessCode = action.prevState, @@ -105,9 +88,6 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail // state should be copied to avoid concurrent modifications from different sources is DetailsAction.AppSettings.Prepare -> state.copy( appSettingsState = state.appSettingsState.copy( - saveWallets = action.state.saveWallets, - saveAccessCodes = action.state.saveAccessCodes, - isBiometricsAvailable = action.state.isBiometricsAvailable, isHidingEnabled = action.state.isHidingEnabled, selectedAppCurrency = action.state.selectedAppCurrency, selectedThemeMode = action.state.selectedThemeMode, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 518cd67afb..83cf685304 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -13,12 +13,6 @@ data class DetailsState( @Suppress("BooleanPropertyNaming") data class AppSettingsState( - @Deprecated("Delete after hot wallet release") - val saveWallets: Boolean = false, - @Deprecated("Delete after hot wallet release") - val saveAccessCodes: Boolean = false, - @Deprecated("Delete after hot wallet release") - val isBiometricsAvailable: Boolean = false, val requireAccessCode: Boolean = false, val useBiometricAuthentication: Boolean = false, val needEnrollBiometrics: Boolean = false, @@ -32,5 +26,5 @@ data class AppSettingsState( enum class SecurityOption { LongTap, PassCode, AccessCode } enum class AppSetting { - SaveWallets, SaveAccessCode, RequireAccessCode, BiometricAuthentication, + RequireAccessCode, BiometricAuthentication, } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt index 73459bc7b9..106fad8b50 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt @@ -9,26 +9,6 @@ import kotlinx.collections.immutable.toImmutableList internal class AppSettingsDialogsFactory { - fun createDeleteSavedWalletsAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { - return Dialog.Alert( - title = resourceReference(R.string.common_attention), - description = resourceReference(R.string.app_settings_off_saved_wallet_alert_message), - confirmText = resourceReference(R.string.common_delete), - onConfirm = onDelete, - onDismiss = onDismiss, - ) - } - - fun createDeleteSavedAccessCodesAlert(onDelete: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { - return Dialog.Alert( - title = resourceReference(R.string.common_attention), - description = resourceReference(R.string.app_settings_off_saved_access_code_alert_message), - confirmText = resourceReference(R.string.common_delete), - onConfirm = onDelete, - onDismiss = onDismiss, - ) - } - fun createThemeModeSelectorDialog( selectedModeIndex: Int, onSelect: (AppThemeMode) -> Unit, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt index 53b58cdd25..f8f30677e8 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsItemsFactory.kt @@ -19,21 +19,6 @@ internal class AppSettingsItemsFactory { ) } - fun createSaveWalletsSwitch( - isChecked: Boolean, - isEnabled: Boolean, - onCheckedChange: (Boolean) -> Unit, - ): Item.Switch { - return Item.Switch( - id = ID_SAVE_WALLETS_SWITCH, - title = resourceReference(R.string.app_settings_saved_wallet), - description = resourceReference(R.string.app_settings_saved_wallet_footer), - isEnabled = isEnabled, - isChecked = isChecked, - onCheckedChange = onCheckedChange, - ) - } - fun createUseBiometricsSwitch( isChecked: Boolean, isEnabled: Boolean, @@ -127,7 +112,6 @@ internal class AppSettingsItemsFactory { companion object { const val ID_ENROLL_BIOMETRICS_CARD = "enroll_biometrics_card" - const val ID_SAVE_WALLETS_SWITCH = "save_wallets_switch" const val ID_SAVE_ACCESS_CODES_SWITCH = "save_access_codes_switch" const val ID_FLIP_TO_HIDE_BALANCE_SWITCH = "flip_to_hide_balance_switch" const val ID_SELECT_APP_CURRENCY_BUTTON = "select_app_currency_button" diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt index 438819ed0a..d17e6b23b6 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt @@ -97,7 +97,6 @@ private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvide val items = persistentListOf( itemsFactory.createEnrollBiometricsCard {}, itemsFactory.createSelectAppCurrencyButton(currentAppCurrencyName = "US Dollar") {}, - itemsFactory.createSaveWalletsSwitch(isChecked = true, isEnabled = true, { _ -> }), itemsFactory.createSaveAccessCodeSwitch(isChecked = false, isEnabled = true) { _ -> }, itemsFactory.createFlipToHideBalanceSwitch(isChecked = false, isEnabled = true) { _ -> }, itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}), diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt index f27a3d76d8..ff7916fd68 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt @@ -43,12 +43,12 @@ private fun AlertDialogPreview(@PreviewParameter(AlertDialogProvider::class) dia } } -private class AlertDialogProvider : CollectionPreviewParameterProvider( +private class AlertDialogProvider : CollectionPreviewParameterProvider( collection = buildList { val dialogsFactory = AppSettingsDialogsFactory() - add(dialogsFactory.createDeleteSavedAccessCodesAlert({}, {})) - add(dialogsFactory.createDeleteSavedWalletsAlert({}, {})) + add(dialogsFactory.createThemeModeSelectorDialog(selectedModeIndex = 0, onSelect = {}, onDismiss = {})) + add(dialogsFactory.createDisableBiometricAuthenticationAlert(onDisable = {}, onDismiss = {})) }, ) // endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt index c5cc2f6f36..3f7613c86c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt @@ -14,10 +14,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.settings.CanUseBiometryUseCase -import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction @@ -52,14 +49,11 @@ internal class AppSettingsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val appCurrencyRepository: AppCurrencyRepository, private val walletsRepository: WalletsRepository, - private val canUseBiometryUseCase: CanUseBiometryUseCase, private val userWalletsListRepository: UserWalletsListRepository, private val balanceHidingRepository: BalanceHidingRepository, private val analyticsEventHandler: AnalyticsEventHandler, private val appThemeModeRepository: AppThemeModeRepository, - private val settingsRepository: SettingsRepository, private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val uiMessageSender: UiMessageSender, ) : Model(), StoreSubscriber { @@ -120,47 +114,25 @@ internal class AppSettingsModel @Inject constructor( ), ) - if (hotWalletFeatureToggles.isHotWalletEnabled) { - val canUseBiometrics = - !state.needEnrollBiometrics && !state.isInProgress && state.hasSecuredWallets + val canUseBiometrics = + !state.needEnrollBiometrics && !state.isInProgress && state.hasSecuredWallets - add( - itemsFactory.createUseBiometricsSwitch( - isChecked = state.useBiometricAuthentication, - isEnabled = canUseBiometrics, - onCheckedChange = ::onBiometricAuthenticationToggled, - onDisabledClick = ::onBiometricAuthenticationDisabledClicked, - ), - ) + add( + itemsFactory.createUseBiometricsSwitch( + isChecked = state.useBiometricAuthentication, + isEnabled = canUseBiometrics, + onCheckedChange = ::onBiometricAuthenticationToggled, + onDisabledClick = ::onBiometricAuthenticationDisabledClicked, + ), + ) - add( - itemsFactory.createRequireAccessCodeSwitch( - isChecked = state.requireAccessCode || !state.useBiometricAuthentication, - isEnabled = canUseBiometrics && state.useBiometricAuthentication, - onCheckedChange = ::onRequireAccessCodeToggled, - ), - ) - } else { - if (state.isBiometricsAvailable) { - val canUseBiometrics = !state.needEnrollBiometrics && !state.isInProgress - - add( - itemsFactory.createSaveWalletsSwitch( - isChecked = state.saveWallets, - isEnabled = canUseBiometrics, - onCheckedChange = ::onSaveWalletsToggled, - ), - ) - - add( - itemsFactory.createSaveAccessCodeSwitch( - isChecked = state.saveAccessCodes, - isEnabled = canUseBiometrics, - onCheckedChange = ::onSaveAccessCodesToggled, - ), - ) - } - } + add( + itemsFactory.createRequireAccessCodeSwitch( + isChecked = state.requireAccessCode || !state.useBiometricAuthentication, + isEnabled = canUseBiometrics && state.useBiometricAuthentication, + onCheckedChange = ::onRequireAccessCodeToggled, + ), + ) add( itemsFactory.createFlipToHideBalanceSwitch( @@ -261,42 +233,6 @@ internal class AppSettingsModel @Inject constructor( } } - private fun onSaveWalletsToggled(isChecked: Boolean) { - if (isChecked) { - onSettingsToggled(AppSetting.SaveWallets, enable = true) - } else { - updateContentState { - copy( - dialog = dialogsFactory.createDeleteSavedWalletsAlert( - onDelete = { - onSettingsToggled(AppSetting.SaveWallets, enable = false) - dismissDialog() - }, - onDismiss = ::dismissDialog, - ), - ) - } - } - } - - private fun onSaveAccessCodesToggled(isChecked: Boolean) { - if (isChecked) { - onSettingsToggled(AppSetting.SaveAccessCode, enable = true) - } else { - updateContentState { - copy( - dialog = dialogsFactory.createDeleteSavedAccessCodesAlert( - onDelete = { - onSettingsToggled(AppSetting.SaveAccessCode, enable = false) - dismissDialog() - }, - onDismiss = ::dismissDialog, - ), - ) - } - } - } - private fun onSettingsToggled(setting: AppSetting, enable: Boolean) { store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting)) } @@ -326,9 +262,6 @@ internal class AppSettingsModel @Inject constructor( private fun bootstrapBiometricsUpdates() = modelScope.launch { val state = AppSettingsState( - saveWallets = walletsRepository.shouldSaveUserWalletsSync(), - saveAccessCodes = settingsRepository.shouldSaveAccessCodes(), - isBiometricsAvailable = canUseBiometryUseCase(), useBiometricAuthentication = walletsRepository.useBiometricAuthentication(), requireAccessCode = walletsRepository.requireAccessCode(), isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings, 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 c53ead9529..6e124933fc 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 @@ -14,12 +14,9 @@ import com.tangem.domain.card.ResetCardUseCase import com.tangem.domain.card.ResetCardUserCodeParams import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.requireColdWallet -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.onUserWalletSelected @@ -52,10 +49,8 @@ internal class ResetCardModel @Inject constructor( private val resetCardUseCase: ResetCardUseCase, private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase, private val deleteWalletUseCase: DeleteWalletUseCase, - private val userWalletsListManager: UserWalletsListManager, private val analyticsEventHandler: AnalyticsEventHandler, private val cardSettingsInteractor: CardSettingsInteractor, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val params = paramsContainer.require() @@ -277,16 +272,7 @@ internal class ResetCardModel @Inject constructor( if (newSelectedWallet != null) { store.dispatchNavigationAction { popTo() } } else { - if (hotWalletFeatureToggles.isHotWalletEnabled) { - store.dispatchNavigationAction { replaceAll(AppRoute.Home()) } - } else { - val isLocked = runCatching { userWalletsListManager.asLockable()?.isLocked }.isSuccess - if (isLocked && userWalletsListManager.hasUserWallets) { - store.dispatchNavigationAction { popTo() } - } else { - store.dispatchNavigationAction { replaceAll(AppRoute.Home()) } - } - } + store.dispatchNavigationAction { replaceAll(AppRoute.Home()) } } } diff --git a/app/src/main/java/com/tangem/tap/features/welcome/component/DefaultWelcomeComponent.kt b/app/src/main/java/com/tangem/tap/features/welcome/component/DefaultWelcomeComponent.kt deleted file mode 100644 index b90ec7a20f..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/component/DefaultWelcomeComponent.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.tap.features.welcome.component - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.tap.features.welcome.model.WelcomeModel -import com.tangem.tap.features.welcome.ui.components.WelcomeScreen -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -internal class DefaultWelcomeComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted params: WelcomeComponent.Params, -) : WelcomeComponent, AppComponentContext by context { - - private val model: WelcomeModel = getOrCreateModel(params) - - @Composable - override fun Content(modifier: Modifier) { - val state by model.state.collectAsStateWithLifecycle() - - WelcomeScreen( - modifier = modifier, - state = state, - ) - } - - @AssistedFactory - interface Factory : WelcomeComponent.Factory { - override fun create(context: AppComponentContext, params: WelcomeComponent.Params): DefaultWelcomeComponent - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt b/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt deleted file mode 100644 index 120b0b5484..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/component/WelcomeComponent.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.tap.features.welcome.component - -import com.tangem.common.routing.entity.InitScreenLaunchMode -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableContentComponent - -interface WelcomeComponent : ComposableContentComponent { - - data class Params( - val launchMode: InitScreenLaunchMode, - ) - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/component/impl/PreviewWelcomeComponent.kt b/app/src/main/java/com/tangem/tap/features/welcome/component/impl/PreviewWelcomeComponent.kt deleted file mode 100644 index 94c08e4f70..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/component/impl/PreviewWelcomeComponent.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.tap.features.welcome.component.impl - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import com.tangem.tap.features.welcome.component.WelcomeComponent -import com.tangem.tap.features.welcome.ui.WelcomeScreenState -import com.tangem.tap.features.welcome.ui.components.WelcomeScreen - -internal class PreviewWelcomeComponent( - private val initialState: WelcomeScreenState = WelcomeScreenState(), -) : WelcomeComponent { - - @Composable - override fun Content(modifier: Modifier) { - WelcomeScreen( - modifier = modifier, - state = initialState, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/di/ComponentModule.kt b/app/src/main/java/com/tangem/tap/features/welcome/di/ComponentModule.kt deleted file mode 100644 index 68f1c6121f..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/di/ComponentModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.tap.features.welcome.di - -import com.tangem.tap.features.welcome.component.DefaultWelcomeComponent -import com.tangem.tap.features.welcome.component.WelcomeComponent -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface ComponentModule { - - @Binds - @Singleton - fun bindWelcomeComponentFactory(factory: DefaultWelcomeComponent.Factory): WelcomeComponent.Factory -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/di/ModelModule.kt b/app/src/main/java/com/tangem/tap/features/welcome/di/ModelModule.kt deleted file mode 100644 index 92e347fe26..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/di/ModelModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.tap.features.welcome.di - -import com.tangem.core.decompose.di.ModelComponent -import com.tangem.core.decompose.model.Model -import com.tangem.tap.features.welcome.model.WelcomeModel -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(ModelComponent::class) -internal interface ModelModule { - - @Binds - @IntoMap - @ClassKey(WelcomeModel::class) - fun bindWelcomeModel(model: WelcomeModel): Model -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt deleted file mode 100644 index fcfcc02601..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/model/WelcomeModel.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.tangem.tap.features.welcome.model - -import com.tangem.common.core.TangemError -import com.tangem.common.routing.entity.InitScreenLaunchMode -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.navigation.finisher.AppFinisher -import com.tangem.domain.wallets.legacy.UserWalletsListError -import com.tangem.tap.common.analytics.events.SignIn -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.core.ui.extensions.TextReference -import com.tangem.tap.features.welcome.component.WelcomeComponent -import com.tangem.tap.features.welcome.redux.WelcomeAction -import com.tangem.tap.features.welcome.redux.WelcomeState -import com.tangem.tap.features.welcome.ui.WelcomeScreenState -import com.tangem.tap.features.welcome.ui.model.WarningModel -import com.tangem.tap.store -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update -import org.rekotlin.StoreSubscriber -import javax.inject.Inject - -// FIXME: Remove redux: [REDACTED_JIRA] -@ModelScoped -internal class WelcomeModel @Inject constructor( - override val dispatchers: CoroutineDispatcherProvider, - private val appFinisher: AppFinisher, - private val analyticsEventsHandler: AnalyticsEventHandler, - paramsContainer: ParamsContainer, -) : Model(), StoreSubscriber { - - private val params: WelcomeComponent.Params = paramsContainer.require() - private val initialState: WelcomeScreenState = WelcomeScreenState( - onPopBack = appFinisher::finish, - onUnlockClick = this::unlockWallets, - onScanCardClick = this::scanCard, - onCloseError = this::closeError, - ) - - val state: MutableStateFlow = MutableStateFlow(initialState) - - init { - subscribeToStoreChanges() - initGlobalState() - - val welcomeAction = when (params.launchMode) { - is InitScreenLaunchMode.WithCardScan -> WelcomeAction.ProceedWithCard - is InitScreenLaunchMode.Standard -> WelcomeAction.ProceedWithBiometrics - } - - store.dispatch(welcomeAction) - } - - private fun unlockWallets() { - analyticsEventsHandler.send(SignIn.ButtonBiometricSignIn()) - store.dispatch(WelcomeAction.ProceedWithBiometrics) - } - - private fun scanCard() { - analyticsEventsHandler.send(SignIn.ButtonCardSignIn()) - store.dispatch(WelcomeAction.ProceedWithCard) - } - - private fun closeError() { - store.dispatch(WelcomeAction.CloseError) - } - - override fun newState(state: WelcomeState) { - val warning = createWarningIfNeeded(state.error) - - this.state.update { prevState -> - prevState.copy( - isUnlockWithBiometricsProgressVisible = state.isUnlockWithBiometricsInProgress, - isUnlockWithCardProgressVisible = state.isUnlockWithCardInProgress, - warning = warning, - error = state.error - ?.takeIf { !it.silent && warning == null } - ?.let { error -> - val messageResId = error.messageResId - if (messageResId != null) { - TextReference.Res(messageResId) - } else { - TextReference.Str(error.customMessage) - } - }, - ) - } - } - - override fun onDestroy() { - store.unsubscribe(subscriber = this) - - super.onDestroy() - } - - private fun createWarningIfNeeded(error: TangemError?): WarningModel? { - return when (error) { - is UserWalletsListError.BiometricsAuthenticationLockout -> WarningModel.BiometricsLockoutWarning( - isPermanent = error.isPermanent, - onDismiss = this::dismissWarning, - ) - is UserWalletsListError.AllKeysInvalidated, - is UserWalletsListError.NoUserWalletSelected, - -> WarningModel.KeyInvalidatedWarning( - onDismiss = this::dismissWarning, - ) - is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning( - onDismiss = this::clearUserWallets, - ) - else -> null - } - } - - private fun dismissWarning() { - state.update { prevState -> - prevState.copy( - warning = null, - ) - } - closeError() - } - - private fun clearUserWallets() { - store.dispatch(WelcomeAction.ClearUserWallets) - } - - private fun subscribeToStoreChanges() { - store.subscribe(this) { appState -> - appState.skip { old, new -> old.welcomeState == new.welcomeState } - .select { it.welcomeState } - } - } - - private fun initGlobalState() { - store.dispatch(GlobalAction.RestoreAppCurrency) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt deleted file mode 100644 index 6d0e6b6183..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.tap.features.welcome.redux - -import com.tangem.common.core.TangemError -import org.rekotlin.Action - -internal sealed interface WelcomeAction : Action { - - data object ProceedWithBiometrics : WelcomeAction { - object Success : WelcomeAction - data class Error(val error: TangemError) : WelcomeAction - } - - object ProceedWithCard : WelcomeAction { - object Success : WelcomeAction - data class Error(val error: TangemError) : WelcomeAction - data class ChangeProgress(val isProgress: Boolean) : WelcomeAction - } - - object CloseError : WelcomeAction - - object ClearUserWallets : WelcomeAction -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt deleted file mode 100644 index 3fea04772f..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.tangem.tap.features.welcome.redux - -import com.tangem.common.core.TangemSdkError -import com.tangem.common.doOnFailure -import com.tangem.common.doOnResult -import com.tangem.common.doOnSuccess -import com.tangem.common.flatMap -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.utils.popTo -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic -import com.tangem.domain.card.analytics.ParamCardCurrencyConverter -import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType -import com.tangem.domain.wallets.legacy.unlockIfLockable -import com.tangem.tap.* -import com.tangem.tap.common.extensions.* -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.proxy.redux.DaggerGraphState -import kotlinx.coroutines.launch -import org.rekotlin.Middleware -import timber.log.Timber - -internal class WelcomeMiddleware { - val middleware: Middleware = { _, appStateProvider -> - { next -> - { action -> - val appState = appStateProvider() - if (action is WelcomeAction && appState != null) { - handleAction(action) - } - next(action) - } - } - } - - private fun handleAction(action: WelcomeAction) { - mainScope.launch { - when (action) { - is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics() - is WelcomeAction.ProceedWithCard -> proceedWithCard() - is WelcomeAction.ClearUserWallets -> disableUserWalletsSaving() - else -> Unit - } - } - } - - private suspend fun proceedWithBiometrics() { - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - userWalletsListManager.unlockIfLockable(type = UnlockType.ANY) - .doOnFailure { error -> - Timber.e(error, "Unable to unlock user wallets with biometrics") - store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Error(error)) - } - .doOnSuccess { selectedUserWallet -> - sendSignedInAnalyticsEvent( - userWallet = selectedUserWallet, - signInType = Basic.SignedInLegacy.SignInType.Biometric, - ) - - store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) } - store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics.Success) - store.onUserWalletSelected(userWallet = selectedUserWallet) - } - } - - private suspend fun proceedWithCard() { - scanCardInternal { scanResponse -> - val userWalletBuilder = store.inject(DaggerGraphState::coldUserWalletBuilderFactory).create(scanResponse) - - val userWallet = userWalletBuilder.build() ?: return@scanCardInternal - - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - userWalletsListManager.save(userWallet, canOverride = true) - .doOnFailure { error -> - Timber.e(error, "Unable to save user wallet") - store.dispatchWithMain(WelcomeAction.ProceedWithCard.Error(error)) - } - .doOnSuccess { - sendSignedInAnalyticsEvent(userWallet, signInType = Basic.SignedInLegacy.SignInType.Card) - - store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) } - store.dispatchWithMain(WelcomeAction.ProceedWithCard.Success) - store.onUserWalletSelected(userWallet = userWallet) - } - } - } - - private fun sendSignedInAnalyticsEvent(userWallet: UserWallet, signInType: Basic.SignedInLegacy.SignInType) { - if (userWallet !is UserWallet.Cold) { - return - } - - val scanResponse = userWallet.scanResponse - val currency = ParamCardCurrencyConverter().convert( - value = scanResponse.cardTypesResolver, - ) - - val trackingContextProxy = store.inject(DaggerGraphState::trackingContextProxy) - trackingContextProxy.addContext(scanResponse) - - if (currency != null) { - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - - Analytics.send( - event = Basic.SignedInLegacy( - currency = currency, - batch = scanResponse.card.batchId, - signInType = signInType, - walletsCount = userWalletsListManager.walletsCount.toString(), - isImported = userWallet.isImported, - hasBackup = scanResponse.card.backupStatus?.isActive, - ), - ) - } - } - - private suspend fun disableUserWalletsSaving() { - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - userWalletsListManager.clear() - .flatMap { tangemSdkManager.clearSavedUserCodes() } - .doOnFailure { e -> - Timber.e(e, "Unable to clear user wallets") - } - .doOnResult { - store.dispatchWithMain(WelcomeAction.CloseError) - store.dispatchNavigationAction { popTo() } - } - } - - private suspend inline fun scanCardInternal(crossinline onCardScanned: suspend (ScanResponse) -> Unit) { - val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() - - store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = shouldSaveAccessCodes, - ) - - store.inject(DaggerGraphState::scanCardProcessor).scan( - analyticsSource = AnalyticsParam.ScreensSources.SignIn, - onSuccess = { scanResponse -> - scope.launch { onCardScanned(scanResponse) } - }, - onFailure = { error -> - when (error) { - is TangemSdkError.ExceptionError -> { - store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success) - } - else -> { - store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(error)) - } - } - }, - onProgressStateChange = { - store.dispatchWithMain(WelcomeAction.ProceedWithCard.ChangeProgress(it)) - }, - onWalletNotCreated = { - store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success) - }, - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt deleted file mode 100644 index f324887d0a..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.tap.features.welcome.redux - -import com.tangem.tap.common.redux.AppState -import org.rekotlin.Action - -internal object WelcomeReducer { - fun reduce(action: Action, state: AppState): WelcomeState { - return if (action is WelcomeAction) { - internalReduce(action, state.welcomeState) - } else { - state.welcomeState - } - } - - private fun internalReduce(action: WelcomeAction, state: WelcomeState): WelcomeState { - return when (action) { - is WelcomeAction.ProceedWithBiometrics -> state.copy(isUnlockWithBiometricsInProgress = true) - is WelcomeAction.ProceedWithCard -> state.copy(isUnlockWithCardInProgress = true) - is WelcomeAction.ProceedWithBiometrics.Error -> state.copy( - error = action.error, - isUnlockWithBiometricsInProgress = false, - ) - is WelcomeAction.ProceedWithCard.Error -> state.copy( - error = action.error, - isUnlockWithCardInProgress = false, - ) - is WelcomeAction.ProceedWithCard.ChangeProgress -> state.copy( - isUnlockWithCardInProgress = action.isProgress, - ) - is WelcomeAction.ProceedWithBiometrics.Success -> state.copy(isUnlockWithBiometricsInProgress = false) - is WelcomeAction.ProceedWithCard.Success -> state.copy(isUnlockWithCardInProgress = false) - is WelcomeAction.CloseError -> state.copy(error = null) - else -> state - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt deleted file mode 100644 index a4fce23f93..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.tap.features.welcome.redux - -import com.tangem.common.core.TangemError -import org.rekotlin.StateType - -data class WelcomeState( - val isUnlockWithBiometricsInProgress: Boolean = false, - val isUnlockWithCardInProgress: Boolean = false, - val error: TangemError? = null, -) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt deleted file mode 100644 index 5c3ddfe725..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeScreenState.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.tap.features.welcome.ui - -import com.tangem.core.ui.extensions.TextReference -import com.tangem.tap.features.welcome.ui.model.WarningModel - -internal data class WelcomeScreenState( - val onPopBack: () -> Unit = {}, - val isUnlockWithBiometricsProgressVisible: Boolean = false, - val isUnlockWithCardProgressVisible: Boolean = false, - val warning: WarningModel? = null, - val error: TextReference? = null, - val onUnlockClick: () -> Unit = {}, - val onScanCardClick: () -> Unit = {}, - val onCloseError: () -> Unit = {}, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt deleted file mode 100644 index 2dc63dd5dd..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt +++ /dev/null @@ -1,136 +0,0 @@ -package com.tangem.tap.features.welcome.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.tap.features.welcome.ui.model.WarningModel -import com.tangem.wallet.R - -@Composable -internal fun WarningDialog(warning: WarningModel?) { - when (warning) { - null -> Unit - is WarningModel.BiometricsLockoutWarning -> { - BasicDialog( - title = stringResourceSafe(id = R.string.biometric_lockout_warning_title), - message = stringResourceSafe( - id = if (warning.isPermanent) { - R.string.biometric_lockout_permanent_warning_description - } else { - R.string.biometric_lockout_warning_description - }, - ), - onDismissDialog = warning.onDismiss, - confirmButton = DialogButtonUM( - title = stringResourceSafe(id = R.string.common_ok), - onClick = warning.onDismiss, - ), - ) - } - is WarningModel.KeyInvalidatedWarning -> { - BasicDialog( - title = stringResourceSafe(id = R.string.common_attention), - message = stringResourceSafe(id = R.string.key_invalidated_warning_description), - onDismissDialog = warning.onDismiss, - confirmButton = DialogButtonUM( - title = stringResourceSafe(id = R.string.common_ok), - onClick = warning.onDismiss, - ), - ) - } - is WarningModel.BiometricsDisabledWarning -> { - BasicDialog( - title = stringResourceSafe(id = R.string.common_warning), - message = stringResourceSafe(id = R.string.biometric_unavailable_warning), - onDismissDialog = warning.onDismiss, - isDismissable = false, - confirmButton = DialogButtonUM( - title = stringResourceSafe(id = R.string.common_ok), - onClick = warning.onDismiss, - ), - ) - } - } -} - -// region Preview -@Composable -private fun BiometricsLockoutDialogSample(modifier: Modifier = Modifier) { - Column(modifier = modifier) { - WarningDialog( - warning = WarningModel.BiometricsLockoutWarning( - isPermanent = false, - onDismiss = {}, - ), - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun BiometricsLockoutDialogPreview() { - TangemThemePreview { - BiometricsLockoutDialogSample() - } -} - -@Composable -private fun BiometricsLockoutDialog_Permanent_Sample(modifier: Modifier = Modifier) { - Column(modifier = modifier) { - WarningDialog( - warning = WarningModel.BiometricsLockoutWarning( - isPermanent = true, - onDismiss = {}, - ), - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun BiometricsLockoutDialog_Permanent_Preview() { - TangemThemePreview { - BiometricsLockoutDialog_Permanent_Sample() - } -} - -@Composable -private fun KeyInvalidatedWarningSample(modifier: Modifier = Modifier) { - Column(modifier = modifier) { - WarningDialog(warning = WarningModel.KeyInvalidatedWarning(onDismiss = {})) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun KeyInvalidatedWarningPreview() { - TangemThemePreview { - KeyInvalidatedWarningSample() - } -} - -@Composable -private fun BiometricDisabledWarningSample(modifier: Modifier = Modifier) { - Column(modifier = modifier) { - WarningDialog(warning = WarningModel.BiometricsDisabledWarning(onDismiss = {})) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun BiometricDisabledWarningPreview() { - TangemThemePreview { - BiometricDisabledWarningSample() - } -} -// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt deleted file mode 100644 index 50cfcc86b1..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreen.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.tangem.tap.features.welcome.ui.components - -import android.content.res.Configuration -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -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.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.tap.features.welcome.component.WelcomeComponent -import com.tangem.tap.features.welcome.component.impl.PreviewWelcomeComponent -import com.tangem.tap.features.welcome.ui.WelcomeScreenState -import com.tangem.tap.features.welcome.ui.model.WarningModel - -@Composable -internal fun WelcomeScreen(state: WelcomeScreenState, modifier: Modifier = Modifier) { - val snackbarHostState = remember { SnackbarHostState() } - val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference()) - val warning by rememberUpdatedState(newValue = state.warning) - - BackHandler(onBack = state.onPopBack) - - Box( - modifier = modifier - .background(TangemTheme.colors.background.primary) - .systemBarsPadding(), - ) { - WelcomeScreenContent( - showUnlockProgress = state.isUnlockWithBiometricsProgressVisible, - showScanCardProgress = state.isUnlockWithCardProgressVisible, - onUnlockClick = state.onUnlockClick, - onScanCardClick = state.onScanCardClick, - ) - - SnackbarHost( - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(vertical = 16.dp) - .fillMaxWidth(), - hostState = snackbarHostState, - ) - } - - WarningDialog(warning) - - LaunchedEffect(errorMessage, state.onCloseError) { - errorMessage?.let { message -> - snackbarHostState.showSnackbar(message) - state.onCloseError() - } - } -} - -// region Preview -@Composable -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun Preview_WelcomeScreen( - @PreviewParameter(WelcomeComponentPreviewProvider::class) component: WelcomeComponent, -) { - TangemThemePreview { - component.Content(Modifier) - } -} - -private class WelcomeComponentPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - PreviewWelcomeComponent(), - PreviewWelcomeComponent( - initialState = WelcomeScreenState( - isUnlockWithBiometricsProgressVisible = true, - isUnlockWithCardProgressVisible = true, - ), - ), - PreviewWelcomeComponent( - initialState = WelcomeScreenState( - error = TextReference.Str(value = "Error"), - ), - ), - PreviewWelcomeComponent( - initialState = WelcomeScreenState( - warning = WarningModel.KeyInvalidatedWarning(onDismiss = {}), - ), - ), - ) -} -// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt deleted file mode 100644 index 433f9521fa..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WelcomeScreenContent.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.tangem.tap.features.welcome.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.* -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.wallet.R - -@Suppress("LongMethod") -@Composable -internal fun WelcomeScreenContent( - showUnlockProgress: Boolean, - showScanCardProgress: Boolean, - onUnlockClick: () -> Unit, - onScanCardClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SpacerHMax() - Icon( - modifier = Modifier.size(TangemTheme.dimens.size96), - painter = painterResource(id = R.drawable.img_tangem_logo_96), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - SpacerH32() - Text( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - text = stringResourceSafe(R.string.welcome_unlock_title), - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - SpacerH12() - Text( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing44) - .fillMaxWidth(), - text = stringResourceSafe( - id = R.string.welcome_unlock_description, - stringResourceSafe(id = R.string.common_biometric_authentication), - ), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - SpacerHMax() - SecondaryButton( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - text = stringResourceSafe( - id = R.string.welcome_unlock, - stringResourceSafe(id = R.string.common_biometrics), - ), - showProgress = showUnlockProgress, - onClick = onUnlockClick, - ) - SpacerH12() - PrimaryButtonIconEnd( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - text = stringResourceSafe(R.string.welcome_unlock_card), - showProgress = showScanCardProgress, - iconResId = R.drawable.ic_tangem_24, - onClick = onScanCardClick, - ) - SpacerH16() - } -} - -// region Preview -@Composable -private fun WelcomeScreenContentSample(modifier: Modifier = Modifier) { - Box( - modifier = modifier - .background(TangemTheme.colors.background.primary), - ) { - WelcomeScreenContent( - showUnlockProgress = false, - showScanCardProgress = false, - onUnlockClick = { /* no-op */ }, - onScanCardClick = { /* no-op */ }, - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun WelcomeScreenContentPreview() { - TangemThemePreview { - WelcomeScreenContentSample() - } -} -// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/model/WarningModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/model/WarningModel.kt deleted file mode 100644 index f8fb28135c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/model/WarningModel.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.tap.features.welcome.ui.model - -internal sealed interface WarningModel { - data class BiometricsLockoutWarning( - val isPermanent: Boolean, - val onDismiss: () -> Unit, - ) : WarningModel - - data class KeyInvalidatedWarning( - val onDismiss: () -> Unit, - ) : WarningModel - - data class BiometricsDisabledWarning( - val onDismiss: () -> Unit, - ) : WarningModel -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt index 79a50c1403..1c102c9e0c 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt @@ -6,14 +6,11 @@ import com.tangem.datasource.api.common.config.ApiEnvironment import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.utils.Provider import com.tangem.utils.ProviderSuspend internal class DefaultAuthProvider( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val shouldUseNewListRepository: Boolean = false, private val environmentConfigStorage: EnvironmentConfigStorage, ) : AuthProvider { @@ -71,18 +68,10 @@ internal class DefaultAuthProvider( } private suspend fun getWallets(): List { - return if (shouldUseNewListRepository) { - userWalletsListRepository.userWalletsSync() - } else { - userWalletsListManager.userWalletsSync - } + return userWalletsListRepository.userWalletsSync() } private suspend fun getSelectedWallet(): UserWallet? { - return if (shouldUseNewListRepository) { - userWalletsListRepository.selectedUserWalletSync() - } else { - userWalletsListManager.selectedUserWalletSync - } + return userWalletsListRepository.selectedUserWalletSync() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index b6f4c2bec8..e6cba79706 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -3,16 +3,10 @@ package com.tangem.tap.network.auth.di import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.P2PEthPoolAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider -import com.tangem.tap.network.auth.DefaultAppVersionProvider -import com.tangem.tap.network.auth.DefaultAuthProvider -import com.tangem.tap.network.auth.DefaultExpressAuthProvider -import com.tangem.tap.network.auth.DefaultP2PEthPoolAuthProvider -import com.tangem.tap.network.auth.DefaultStakeKitAuthProvider +import com.tangem.tap.network.auth.* import com.tangem.utils.version.AppVersionProvider import dagger.Module import dagger.Provides @@ -27,15 +21,11 @@ internal class AuthModule { @Provides @Singleton fun provideAuthProvider( - userWalletsListManager: UserWalletsListManager, userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, environmentConfigStorage: EnvironmentConfigStorage, ): AuthProvider { return DefaultAuthProvider( - userWalletsListManager = userWalletsListManager, userWalletsListRepository = userWalletsListRepository, - shouldUseNewListRepository = hotWalletFeatureToggles.isHotWalletEnabled, environmentConfigStorage = environmentConfigStorage, ) } 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 8095c3df87..133c325afc 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 @@ -31,9 +31,7 @@ import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.hot.sdk.TangemHotSdk import com.tangem.operations.attestation.CardArtworksProvider @@ -53,7 +51,6 @@ data class DaggerGraphState( val appThemeModeRepository: AppThemeModeRepository? = null, val balanceHidingRepository: BalanceHidingRepository? = null, val walletsRepository: WalletsRepository? = null, - val generalUserWalletsListManager: UserWalletsListManager? = null, val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase? = null, val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase? = null, val cardRepository: CardRepository? = null, @@ -78,7 +75,6 @@ data class DaggerGraphState( val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory? = null, val userTokensResponseStore: UserTokensResponseStore? = null, val userWalletsListRepository: UserWalletsListRepository? = null, - val hotWalletFeatureToggles: HotWalletFeatureToggles? = null, val tangemHotSdk: TangemHotSdk? = null, val trackingContextProxy: TrackingContextProxy? = null, ) : StateType \ 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 23bc630cec..52a03dcf84 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 @@ -29,7 +29,6 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository 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.HotWalletFeatureToggles import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy import com.tangem.features.walletconnect.components.WcRoutingComponent import com.tangem.hot.sdk.TangemHotSdk @@ -69,7 +68,6 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private val userWalletsListRepository: UserWalletsListRepository, private val cardRepository: CardRepository, private val onboardingRepository: OnboardingRepository, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsExceptionHandler: AnalyticsExceptionHandler, @@ -254,16 +252,14 @@ internal class DefaultRoutingComponent @AssistedInject constructor( } private suspend fun trackSignInEvent() { - if (hotWalletFeatureToggles.isHotWalletEnabled) { - val userWallets = userWalletsListRepository.userWalletsSync() - val selectedWallet = userWalletsListRepository.selectedUserWalletSync() ?: return - trackingContextProxy.addContext(selectedWallet) - analyticsEventHandler.send( - event = Basic.SignedIn( - signInType = Basic.SignedIn.SignInType.NoSecurity, - walletsCount = userWallets.size, - ), - ) - } + val userWallets = userWalletsListRepository.userWalletsSync() + val selectedWallet = userWalletsListRepository.selectedUserWalletSync() ?: return + trackingContextProxy.addContext(selectedWallet) + analyticsEventHandler.send( + event = Basic.SignedIn( + signInType = Basic.SignedIn.SignInType.NoSecurity, + walletsCount = userWallets.size, + ), + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index f182a2555b..cbc3285d4f 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 @@ -52,7 +52,6 @@ import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent import com.tangem.tap.features.details.ui.cardsettings.coderecovery.api.AccessCodeRecoveryComponent import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent -import com.tangem.tap.features.welcome.component.WelcomeComponent import com.tangem.tap.routing.component.RoutingComponent.Child import dagger.hilt.android.scopes.ActivityScoped import javax.inject.Inject @@ -76,7 +75,6 @@ internal class ChildFactory @Inject constructor( private val sellCryptoComponentFactory: SellCryptoComponent.Factory, private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory, private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory, - private val welcomeComponentFactory: WelcomeComponent.Factory, private val newWelcomeComponentFactory: NewWelcomeComponent.Factory, private val storiesComponentFactory: StoriesComponent.Factory, private val stakingComponentFactory: StakingComponent.Factory, @@ -118,7 +116,6 @@ internal class ChildFactory @Inject constructor( private val tangemPayOnboardingComponentFactory: TangemPayOnboardingComponent.Factory, private val kycComponentFactory: KycComponent.Factory, private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val feedEntryComponentFactory: FeedEntryComponent.Factory, private val feedFeatureToggle: FeedFeatureToggle, ) { @@ -162,21 +159,11 @@ internal class ChildFactory @Inject constructor( ) } is AppRoute.Welcome -> { - if (hotWalletFeatureToggles.isHotWalletEnabled) { - createComponentChild( - context = context, - params = Unit, - componentFactory = newWelcomeComponentFactory, - ) - } else { - createComponentChild( - context = context, - params = WelcomeComponent.Params( - launchMode = route.launchMode, - ), - componentFactory = welcomeComponentFactory, - ) - } + createComponentChild( + context = context, + params = Unit, + componentFactory = newWelcomeComponentFactory, + ) } is AppRoute.WalletSettings -> { createComponentChild( diff --git a/app/src/test/kotlin/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManagerTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManagerTest.kt deleted file mode 100644 index 7e54ffc6bd..0000000000 --- a/app/src/test/kotlin/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManagerTest.kt +++ /dev/null @@ -1,216 +0,0 @@ -package com.tangem.tap.domain.userWalletList.implementation - -import com.google.common.truth.Truth -import com.tangem.common.test.domain.card.MockScanResponseFactory -import com.tangem.domain.card.configs.GenericCardConfig -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import io.mockk.mockk -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized -import kotlin.reflect.full.declaredFunctions -import kotlin.reflect.jvm.isAccessible - -/** -[REDACTED_AUTHOR] - */ -@RunWith(Parameterized::class) -internal class BiometricUserWalletsListManagerTest(private val model: Model) { - - private val manager = BiometricUserWalletsListManager( - keysRepository = mockk(), - publicInformationRepository = mockk(), - sensitiveInformationRepository = mockk(), - selectedUserWalletRepository = mockk(), - dispatcherProvider = mockk(), - ) - - @Test - fun testFindAvailableUserWallet() { - with(model) { - val actual = userWallets.findAvailableUserWallet(prevSelectedIndex) - - Truth.assertThat(actual).isEqualTo(newSelectedWallet) - } - } - - private fun List.findAvailableUserWallet(prevSelectedIndex: Int): UserWallet? { - return manager::class - .declaredFunctions - .firstOrNull { it.name == "findAvailableUserWallet" } - ?.apply { isAccessible = true } - ?.call(manager, this, prevSelectedIndex) - as? UserWallet - } - - data class Model( - val userWallets: List, - val prevSelectedIndex: Int, - val newSelectedWallet: UserWallet?, - ) - - private companion object { - - val userWallet0 = createUserWallet(id = "0", isLocked = false) - val lockedUserWallet0 = createUserWallet(id = "0", isLocked = true) - - val userWallet1 = createUserWallet(id = "1", isLocked = false) - val lockedUserWallet1 = createUserWallet(id = "1", isLocked = true) - - val userWallet2 = createUserWallet(id = "2", isLocked = false) - val lockedUserWallet2 = createUserWallet(id = "2", isLocked = true) - - val userWallet3 = createUserWallet(id = "3", isLocked = false) - val lockedUserWallet3 = createUserWallet(id = "3", isLocked = true) - - val unlockedWallets = listOf(userWallet0, userWallet1, userWallet2, userWallet3) - val lockedWallets = listOf(lockedUserWallet0, lockedUserWallet1, lockedUserWallet2, lockedUserWallet3) - - @JvmStatic - @Parameterized.Parameters - fun data(): Collection { - return listOf( - Model(userWallets = emptyList(), prevSelectedIndex = 0, newSelectedWallet = null), - *getTestsWithUnlockedWallets().toTypedArray(), - *getTestsIfPrevSelectedIndexIs0().toTypedArray(), - *getTestsIfPrevSelectedIndexIsLastIndex().toTypedArray(), - *getTestsIfNewSelectedIndexIsNearby().toTypedArray(), - *getTestsIfNewSelectedIndexIsThroughOne().toTypedArray(), - ) - } - - fun getTestsWithUnlockedWallets() = listOf( - // [*0*, 1, 2, 3, 4] => delete 0 => [1, 2, 3, 4] => select 1 => [*1*, 2, 3, 4] - Model(userWallets = unlockedWallets, prevSelectedIndex = 0, newSelectedWallet = userWallet0), - // [0, *1*, 2, 3, 4] => delete 1 => [0, 2, 3, 4] => select 2 => [0, *2*, 3, 4] - Model(userWallets = unlockedWallets, prevSelectedIndex = 1, newSelectedWallet = userWallet1), - // [0, 1, *2*, 3, 4] => delete 2 => [0, 1, 3, 4] => select 3 => [0, 1, *3*, 4] - Model(userWallets = unlockedWallets, prevSelectedIndex = 2, newSelectedWallet = userWallet2), - // [0, 1, 2, *3*, 4] => delete 3 => [0, 1, 2, 4] => select 4 => [0, 1, 2, 4] - Model(userWallets = unlockedWallets, prevSelectedIndex = 3, newSelectedWallet = userWallet3), - // [0, 1, 2, 3, *4*] => delete 4 => [0, 1, 2, 3] => select 3 => [0, 1, 2, *3*] - Model(userWallets = unlockedWallets, prevSelectedIndex = 4, newSelectedWallet = userWallet3), - ) - - fun getTestsIfPrevSelectedIndexIs0() = listOf( - // [*0*, -1-, 2, 3, 4] => delete 0 => [-1-, 2, 3, 4] => select 2 => [-1-, *2*, 3, 4] - Model( - userWallets = listOf(lockedUserWallet0, userWallet1, userWallet2, userWallet3), - prevSelectedIndex = 0, - newSelectedWallet = userWallet1, - ), - // [*0*, -1-, -2-, 3, 4] => delete 0 => [-1-, -2-, 3, 4] => select 3 => [-1-, -2-, *3*, 4] - Model( - userWallets = listOf(lockedUserWallet0, lockedUserWallet1, userWallet2, userWallet3), - prevSelectedIndex = 0, - newSelectedWallet = userWallet2, - ), - // [*0*, -1-, -2-, -3-, 4] => delete 0 => [-1-, -2-, -3-, 4] => select 4 => [-1-, -2-, -3-, *4*] - Model( - userWallets = listOf(lockedUserWallet0, lockedUserWallet1, lockedUserWallet2, userWallet3), - prevSelectedIndex = 0, - newSelectedWallet = userWallet3, - ), - // [*0*, -1-, -2-, -3-, -4-] => delete 0 => [-1-, -2-, -3-, -4-] => select 1 => [*-1-*, -2-, -3-, -4-] - Model(userWallets = lockedWallets, prevSelectedIndex = 0, newSelectedWallet = lockedUserWallet0), - ) - - fun getTestsIfPrevSelectedIndexIsLastIndex() = listOf( - // [0, 1, 2, -3-, *4*] => delete 4 => [0, 1, 2, -3-] => select 2 => [0, 1, *2*, -3-] - Model( - userWallets = listOf(userWallet0, userWallet1, userWallet2, lockedUserWallet3), - prevSelectedIndex = 4, - newSelectedWallet = userWallet2, - ), - // [0, 1, -2-, -3-, *4*] => delete 4 => [0, 1, -2-, -3-] => select 1 => [0, *1*, -2-, -3-] - Model( - userWallets = listOf(userWallet0, userWallet1, lockedUserWallet2, lockedUserWallet3), - prevSelectedIndex = 4, - newSelectedWallet = userWallet1, - ), - // [0, -1-, -2-, -3-, *4*] => delete 4 => [0, -1-, -2-, -3-] => select 0 => [*0*, -1-, -2-, -3-] - Model( - userWallets = listOf(userWallet0, lockedUserWallet1, lockedUserWallet2, lockedUserWallet3), - prevSelectedIndex = 4, - newSelectedWallet = userWallet0, - ), - // [-0-, -1-, -2-, -3-, *4*] => delete 4 => [-0-, -1-, -2-, -3-] => select 3 => [-0-, -1-, -2-, *-3-*] - Model(userWallets = lockedWallets, prevSelectedIndex = 4, newSelectedWallet = lockedUserWallet3), - ) - - fun getTestsIfNewSelectedIndexIsNearby() = listOf( - // [0, *1*, 2, 3, 4] => delete 1 => [0, 2, 3, 4] => select 2 => [0, *2*, 3, 4] - Model(userWallets = unlockedWallets, prevSelectedIndex = 1, newSelectedWallet = userWallet1), - // [0, *1*, -2-, 3, 4] => delete 1 => [0, -2-, 3, 4] => select 3 => [0, -2-, *3*, 4] - Model( - userWallets = listOf(userWallet0, lockedUserWallet1, userWallet2, userWallet3), - prevSelectedIndex = 1, - newSelectedWallet = userWallet2, - ), - // [0, *1*, -2-, -3-, 4] => delete 1 => [0, -2-, -3-, 4] => select 0 => [*0*, -2-, -3-, 4] - Model( - userWallets = listOf(userWallet0, lockedUserWallet1, lockedUserWallet2, userWallet3), - prevSelectedIndex = 1, - newSelectedWallet = userWallet0, - ), - // [0, 1, 2, *3*, 4] => delete 3 => [0, 1, 2, 4] => select 4 => [0, 1, 2, *4*] - Model(userWallets = unlockedWallets, prevSelectedIndex = 3, newSelectedWallet = userWallet3), - // [0, 1, 2, *3*, -4-] => delete 3 => [0, 1, 2, -4-] => select 2 => [0, 1, *2*, -4-] - Model( - userWallets = listOf(userWallet0, userWallet1, userWallet2, lockedUserWallet3), - prevSelectedIndex = 3, - newSelectedWallet = userWallet2, - ), - ) - - fun getTestsIfNewSelectedIndexIsThroughOne() = listOf( - // [-0-, *1*, -2-, 3, 4] => delete 1 => [-0-, -2-, 3, 4] => select 3 => [-0-, -2-, *3*, 4] - Model( - userWallets = listOf(lockedUserWallet0, lockedUserWallet1, userWallet2, userWallet3), - prevSelectedIndex = 1, - newSelectedWallet = userWallet2, - ), - // [-0-, *1*, -2-, -3-, 4] => delete 1 => [-0-, -2-, -3-, 4] => select 4 => [-0-, -2-, -3-, *4*] - Model( - userWallets = listOf(lockedUserWallet0, lockedUserWallet1, lockedUserWallet2, userWallet3), - prevSelectedIndex = 1, - newSelectedWallet = userWallet3, - ), - // [0, 1, -2-, *3*, -4-] => delete 3 => [0, 1, -2-, -4-] => select 1 => [0, *1*, -2-, -4-] - Model( - userWallets = listOf(userWallet0, userWallet1, lockedUserWallet2, lockedUserWallet3), - prevSelectedIndex = 3, - newSelectedWallet = userWallet1, - ), - // [0, -1-, -2-, *3*, -4-] => delete 3 => [*0*, -1-, -2-, -4-] => select 0 => [0, -1-, -2-, -4-] - Model( - userWallets = listOf(userWallet0, lockedUserWallet1, lockedUserWallet2, lockedUserWallet3), - prevSelectedIndex = 3, - newSelectedWallet = userWallet0, - ), - ) - - fun createUserWallet(id: String, isLocked: Boolean): UserWallet { - return UserWallet.Cold( - name = "Wallet $id", - walletId = UserWalletId(stringValue = id), - cardsInWallet = emptySet(), - isMultiCurrency = true, - hasBackupError = false, - scanResponse = MockScanResponseFactory.create( - cardConfig = GenericCardConfig(maxWalletCount = 1), - derivedKeys = emptyMap(), - ).let { - if (isLocked) { - it.copy( - card = it.card.copy(wallets = emptyList()), - ) - } else { - it - } - }, - ) - } - } -} \ 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 299c0abe09..1490dbb009 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 @@ -31,18 +31,10 @@ "name": "SWAP_REDESIGN_ENABLED", "version": "undefined" }, - { - "name": "HOT_WALLET_ENABLED", - "version": "5.32.0" - }, { "name": "HOT_WALLET_CREATION_RESTRICTION_ENABLED", "version": "5.32.0" }, - { - "name": "HOT_WALLET_VISIBLE", - "version": "5.32.1" - }, { "name": "TANGEM_PAY_ENABLED", "version": "5.31.0" 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 24ce5d7b07..ca09522466 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 @@ -13,7 +13,6 @@ import com.tangem.domain.feedback.repository.FeedbackRepository 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.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.utils.version.AppVersionProvider import kotlinx.coroutines.flow.MutableStateFlow @@ -26,7 +25,6 @@ import java.io.File * * @property appLogsStore app logs store * @property userWalletsListManager user wallets list manager - * @property shouldUseNewUserWalletsRepository flag to use new user wallets repository * @property userWalletsListRepository user wallets repository * @property walletManagersStore wallet managers store * @property emailSender email sender @@ -37,9 +35,7 @@ import java.io.File @Suppress("LongParameterList") internal class DefaultFeedbackRepository( private val appLogsStore: AppLogsStore, - private val shouldUseNewUserWalletsRepository: Boolean, private val userWalletsListRepository: UserWalletsListRepository, - private val userWalletsListManager: UserWalletsListManager, private val walletManagersStore: WalletManagersStore, private val emailSender: EmailSender, private val appVersionProvider: AppVersionProvider, @@ -126,18 +122,10 @@ internal class DefaultFeedbackRepository( } private suspend fun getUserWalletById(userWalletId: UserWalletId): UserWallet? { - return if (shouldUseNewUserWalletsRepository) { - userWalletsListRepository.userWalletsSync().find { it.walletId == userWalletId } - } else { - userWalletsListManager.userWalletsSync.find { it.walletId == userWalletId } - } + return userWalletsListRepository.userWalletsSync().find { it.walletId == userWalletId } } private fun totalUserWallets(): Int { - return if (shouldUseNewUserWalletsRepository) { - userWalletsListRepository.userWallets.value?.size ?: 0 - } else { - userWalletsListManager.walletsCount - } + return userWalletsListRepository.userWallets.value?.size ?: 0 } } \ No newline at end of file diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt index d6ecc8ef1e..db803a21d2 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt @@ -9,9 +9,7 @@ import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.feedback.repository.FeedbackFeatureToggles import com.tangem.domain.feedback.repository.FeedbackRepository -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.version.AppVersionProvider import dagger.Module import dagger.Provides @@ -27,9 +25,7 @@ internal object FeedbackModule { @Singleton fun provideFeedbackRepository( appLogsStore: AppLogsStore, - userWalletsListManager: UserWalletsListManager, userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, walletManagersStore: WalletManagersStore, emailSender: EmailSender, appVersionProvider: AppVersionProvider, @@ -37,12 +33,10 @@ internal object FeedbackModule { ): FeedbackRepository { return DefaultFeedbackRepository( appLogsStore = appLogsStore, - userWalletsListManager = userWalletsListManager, walletManagersStore = walletManagersStore, emailSender = emailSender, appVersionProvider = appVersionProvider, userWalletsListRepository = userWalletsListRepository, - shouldUseNewUserWalletsRepository = hotWalletFeatureToggles.isHotWalletEnabled, getSelectedWalletUseCase = getSelectedWalletUseCase, ) } 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 7225c10f60..311260900e 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 @@ -7,8 +7,6 @@ import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.repository.OnboardingRepository -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.* @@ -19,9 +17,7 @@ import javax.inject.Inject internal class DefaultTangemPayEligibilityManager @Inject constructor( dispatchers: CoroutineDispatcherProvider, - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val onboardingRepository: OnboardingRepository, ) : TangemPayEligibilityManager { @@ -73,11 +69,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( return emptyList() } - val wallets = if (hotWalletFeatureToggles.isHotWalletEnabled) { - userWalletsListRepository.userWallets.value - } else { - userWalletsListManager.userWalletsSync - } ?: return emptyList() + val wallets = userWalletsListRepository.userWallets.value ?: return emptyList() return wallets.filter { wallet -> wallet.isMultiCurrency && !wallet.isLocked && wallet.isCompatible() @@ -111,11 +103,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( private fun resetDataWhenWalletsUpdate() { coroutineScope.launch { - if (hotWalletFeatureToggles.isHotWalletEnabled) { - userWalletsListRepository.userWallets.collectLatest { reset() } - } else { - userWalletsListManager.userWallets.collectLatest { reset() } - } + userWalletsListRepository.userWallets.collectLatest { reset() } } } 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 0872260f70..2fdcfabcb6 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 @@ -19,8 +19,6 @@ import com.tangem.domain.pay.model.CustomerInfo.ProductInstance import com.tangem.domain.pay.repository.OnboardingRepository import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.TangemPayCardFrozenState -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -42,9 +40,7 @@ internal class DefaultOnboardingRepository @Inject constructor( private val tangemPayStorage: TangemPayStorage, private val authDataSource: TangemPayAuthDataSource, private val cardFrozenStateStore: TangemPayCardFrozenStateStore, - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : OnboardingRepository { // Save data for a session @@ -133,12 +129,8 @@ internal class DefaultOnboardingRepository @Inject constructor( } private fun getUserWallet(userWalletId: UserWalletId): UserWallet { - val userWallet = if (hotWalletFeatureToggles.isHotWalletEnabled) { - userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId } - } else { - userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == userWalletId } - } ?: error("no userWallet found") - return userWallet + return userWalletsListRepository.userWallets.value?.firstOrNull { it.walletId == userWalletId } + ?: error("no userWallet found") } private suspend fun getCustomerInfo( 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 d2e807c047..cccf49fea8 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 @@ -20,7 +20,10 @@ 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.* +import com.tangem.datasource.local.preferences.utils.getObjectMap +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.datasource.local.preferences.utils.getSyncOrNull +import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.models.wallet.UserWallet @@ -54,21 +57,6 @@ internal class DefaultWalletsRepository( private val moshi: com.squareup.moshi.Moshi, ) : WalletsRepository { - @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") - override suspend fun shouldSaveUserWalletsSync(): Boolean { - return appPreferencesStore.getSyncOrDefault(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) - } - - @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") - override fun shouldSaveUserWallets(): Flow { - return appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false) - } - - @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") - override suspend fun saveShouldSaveUserWallets(item: Boolean) { - appPreferencesStore.store(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, value = item) - } - override suspend fun useBiometricAuthentication(): Boolean { val shouldUseBiometricAuth = appPreferencesStore.getSyncOrNull( key = PreferencesKeys.USE_BIOMETRIC_AUTHENTICATION_KEY, diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt index aaf389d312..f2e4b3fbf1 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt @@ -7,12 +7,9 @@ import com.tangem.domain.card.common.extensions.supportedBlockchains import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.requireUserWalletsSync import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.legacy.UserWalletsListManager class FilterAvailableNetworksForWalletUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val shouldUseNewRepository: Boolean, private val excludedBlockchains: ExcludedBlockchains, ) { @@ -24,9 +21,9 @@ class FilterAvailableNetworksForWalletUseCase( userWalletId: UserWalletId, networks: Set, ): Set { - val userWallet = getWallets().firstOrNull { - it.walletId == userWalletId - } ?: return networks.toSet() + val userWallet = userWalletsListRepository.requireUserWalletsSync() + .firstOrNull { it.walletId == userWalletId } + ?: return networks.toSet() val supportedBlockchains = userWallet.supportedBlockchains( excludedBlockchains = excludedBlockchains, @@ -37,10 +34,4 @@ class FilterAvailableNetworksForWalletUseCase( supportedBlockchains.contains(blockchain) }.toSet() } - - private fun getWallets() = if (shouldUseNewRepository) { - userWalletsListRepository.requireUserWalletsSync() - } else { - userWalletsListManager.userWalletsSync - } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt index 8af6e7c2ee..8cfc761703 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/delegate/DefaultUserWalletsSyncDelegate.kt @@ -3,22 +3,15 @@ package com.tangem.domain.wallets.delegate import arrow.core.Either import arrow.core.raise.either import arrow.core.raise.ensure -import com.tangem.common.CompletionResult import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.copy -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.wallets.models.UserWalletRemoteInfo -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext class DefaultUserWalletsSyncDelegate( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val useNewRepository: Boolean, - private val dispatchers: CoroutineDispatcherProvider, ) : UserWalletsSyncDelegate { override suspend fun syncWallet(userWalletId: UserWalletId, name: String): Either { @@ -34,15 +27,6 @@ class DefaultUserWalletsSyncDelegate( private suspend fun renameUserWallet( userWalletId: UserWalletId, name: String, - ): Either = if (useNewRepository) { - renameUserWalletInNewRepository(userWalletId, name) - } else { - renameUserWalletInLegacyRepository(userWalletId, name) - } - - private suspend fun renameUserWalletInNewRepository( - userWalletId: UserWalletId, - name: String, ): Either = either { val userWallets = userWalletsListRepository.userWalletsSync() val userWallet = userWallets.find { it.walletId == userWalletId } @@ -60,34 +44,7 @@ class DefaultUserWalletsSyncDelegate( userWalletsListRepository.saveWithoutLock(updatedWallet, canOverride = true) .map { updatedWallet } - .mapLeft { error -> UpdateWalletError.DataError(IllegalStateException("")) } + .mapLeft { error -> UpdateWalletError.DataError(IllegalStateException("$error")) } .bind() } - - // TODO remove dispatchers whnen UserWalletsListManager will be main safe - private suspend fun renameUserWalletInLegacyRepository( - userWalletId: UserWalletId, - name: String, - ): Either = withContext(dispatchers.io) { - either { - val existingNames = userWalletsListManager.userWalletsSync - - ensure(existingNames.none { it.name == name && it.walletId != userWalletId }) { - UpdateWalletError.NameAlreadyExists - } - - val previousName = existingNames.firstOrNull { it.walletId == userWalletId }?.name.orEmpty() - if (previousName == name) { - raise(UpdateWalletError.NameAlreadyExists) - } - - when ( - val result = - userWalletsListManager.update(userWalletId) { it.copy(name = name) } - ) { - is CompletionResult.Failure -> raise(UpdateWalletError.DataError(result.error)) - is CompletionResult.Success -> result.data - } - } - } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListError.kt deleted file mode 100644 index f287da375c..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListError.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.tangem.domain.wallets.legacy - -import com.tangem.common.core.TangemError -import com.tangem.domain.wallets.R - -sealed class UserWalletsListError(code: Int) : TangemError(code) { - - override val silent: Boolean - get() = (cause as? TangemError)?.silent == true - - override val messageResId: Int? = null - - object WalletAlreadySaved : UserWalletsListError(code = 60001) { - override var customMessage: String = "This wallet has already been saved, you can add another one" - override val messageResId: Int = R.string.user_wallet_list_error_wallet_already_saved - } - - object AllKeysInvalidated : UserWalletsListError(code = 60002) { - override var customMessage: String = "Encryption key invalidated" - } - - data class BiometricsAuthenticationLockout(val isPermanent: Boolean) : UserWalletsListError(code = 60003) { - override var customMessage: String = "Biometric authentication lockout, permanent: $isPermanent" - } - - data class UnableToUnlockUserWallets(override val cause: Throwable? = null) : UserWalletsListError(code = 60004) { - override var customMessage: String = "An error has occurred, please scan your card to log in" - override val messageResId: Int = R.string.user_wallet_list_error_unable_to_unlock - } - - object BiometricsAuthenticationDisabled : UserWalletsListError(code = 60005) { - override var customMessage: String = "Biometrics authentication disabled" - } - - object NoUserWalletSelected : UserWalletsListError(code = 60006) { - override var customMessage: String = "No user wallet selected" - } - - object NotAllUserWalletsUnlocked : UserWalletsListError(code = 60007) { - override var customMessage: String = "Not all user wallets was unlocked" - } -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt deleted file mode 100644 index cb47f21996..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/legacy/UserWalletsListManagerExtensions.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.domain.wallets.legacy - -import com.tangem.common.CompletionResult -import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType -import com.tangem.domain.models.wallet.UserWallet -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf - -/** - * Indicates that the [UserWalletsListManager] is locked - * - * @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] returns [Flow] which - * produces only one false value - * - * @see UserWalletsListManager.Lockable.isLocked - * */ -val UserWalletsListManager.isLocked: Flow - get() = asLockable()?.lockedState ?: flowOf(false) - -/** - * Indicates that the [UserWalletsListManager] is locked - * - * @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] returns false - * - * @see UserWalletsListManager.Lockable.isLocked - * */ -val UserWalletsListManager.isLockedSync: Boolean - get() = asLockable()?.isLocked == true - -/** - * Call [UserWalletsListManager.Lockable.unlock] if [UserWalletsListManager] implements [UserWalletsListManager.Lockable] - * - * @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] - * returns [CompletionResult.Failure] with [UserWalletsListError.UnableToUnlockUserWallets] - * - * If [UserWalletsListManager] implements [UserWalletsListManager.Lockable] - * returns [CompletionResult.Success] with selected [UserWallet] - * - * @see UserWalletsListManager.Lockable.unlock - * */ -suspend fun UserWalletsListManager.unlockIfLockable(type: UnlockType = UnlockType.ANY): CompletionResult { - return asLockable()?.unlock(type) ?: CompletionResult.Failure(UserWalletsListError.UnableToUnlockUserWallets()) -} - -/** - * Safe cast [UserWalletsListManager] to [UserWalletsListManager.Lockable] - * - * @return If [UserWalletsListManager] not implements [UserWalletsListManager.Lockable] then returns null or - * [UserWalletsListManager.Lockable] otherwise - * */ -fun UserWalletsListManager.asLockable(): UserWalletsListManager.Lockable? { - if (this.isLockable) { - return this as? UserWalletsListManager.Lockable - } - return null -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UnlockWalletsError.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UnlockWalletsError.kt deleted file mode 100644 index a6558fb0dd..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/UnlockWalletsError.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.domain.wallets.models - -sealed class UnlockWalletsError { - - object UnableToUnlockWallets : UnlockWalletsError() - - object NoUserWalletSelected : UnlockWalletsError() - - object NotAllUserWalletsUnlocked : UnlockWalletsError() - - data class DataError(val cause: Throwable) : UnlockWalletsError() -} \ 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 1431dc5e6f..da0352f7fd 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 @@ -11,15 +11,6 @@ import kotlinx.coroutines.flow.Flow @Suppress("TooManyFunctions") interface WalletsRepository { - @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") - suspend fun shouldSaveUserWalletsSync(): Boolean - - @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") - fun shouldSaveUserWallets(): Flow - - @Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") - suspend fun saveShouldSaveUserWallets(item: Boolean) - suspend fun useBiometricAuthentication(): Boolean suspend fun setUseBiometricAuthentication(value: Boolean) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt index 22123c07a7..4037df2361 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/DeleteWalletUseCase.kt @@ -1,24 +1,19 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either -import arrow.core.raise.either -import com.tangem.common.doOnFailure import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.error.DeleteWalletError import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.legacy.UserWalletsListManager /** * Use case for deleting user wallet * - * @property userWalletsListManager user wallets list manager + * @property userWalletsListRepository repository for getting list of user wallets * [REDACTED_AUTHOR] */ class DeleteWalletUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val useNewRepository: Boolean, ) { /** @@ -29,19 +24,8 @@ class DeleteWalletUseCase( * @return [Either] with [com.tangem.domain.common.wallets.error.DeleteWalletError] or [Boolean] which indicates that there are still saved wallets. * */ suspend operator fun invoke(userWalletId: UserWalletId): Either { - if (useNewRepository) { - return userWalletsListRepository.delete(userWalletIds = listOf(userWalletId)).map { - userWalletsListRepository.selectedUserWallet.value != null - } - } - - return either { - userWalletsListManager.delete(userWalletIds = listOf(userWalletId)) - .doOnFailure { - raise(DeleteWalletError.UnableToDelete) - } - - userWalletsListManager.hasUserWallets + return userWalletsListRepository.delete(userWalletIds = listOf(userWalletId)).map { + userWalletsListRepository.selectedUserWallet.value != null } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt index 55a32a38ba..3e4f011fee 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateWalletNameUseCase.kt @@ -1,19 +1,16 @@ package com.tangem.domain.wallets.usecase -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.requireUserWalletsSync +import com.tangem.domain.demo.models.DemoConfig +import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ProductType -import com.tangem.domain.wallets.legacy.UserWalletsListManager /** * Use case for user wallet name generation */ class GenerateWalletNameUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val useNewRepository: Boolean, ) { private val CardDTO.isBackupNotAllowed: Boolean @@ -38,11 +35,7 @@ class GenerateWalletNameUseCase( } private fun getNamesSet(): Set { - return if (useNewRepository) { - userWalletsListRepository.requireUserWalletsSync().map { it.name }.toSet() - } else { - userWalletsListManager.userWalletsSync.map { it.name }.toSet() - } + return userWalletsListRepository.requireUserWalletsSync().map { it.name }.toSet() } private fun suggestedWalletName(defaultName: String, existingNames: Set): String { diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt index 2784698845..0be243b67a 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt @@ -2,34 +2,14 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.legacy.asLockable -import com.tangem.domain.wallets.legacy.isLockedSync import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map class GetSavedWalletsCountUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val useNewRepository: Boolean, ) { operator fun invoke(): Flow> { - if (useNewRepository) { - return userWalletsListRepository.userWallets.map { requireNotNull(it) } - } - - return userWalletsListManager.savedWalletsCount - .filter { count -> - if (count == 0) return@filter true - userWalletsListManager.asLockable() ?: return@filter false - return@filter userWalletsListManager.isLockedSync.not() - } - .map { - userWalletsListManager.userWalletsSync - } - .distinctUntilChanged() + return userWalletsListRepository.userWallets.map { requireNotNull(it) } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt index 549813b0bf..071bc36c87 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletSyncUseCase.kt @@ -2,39 +2,26 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either import arrow.core.raise.either -import arrow.core.raise.ensureNotNull import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError /** * Use case for getting selected wallet. * Important! If all wallets is locked, use case returns a error. * - * @property userWalletsListManager user wallets list manager + * @property userWalletsListRepository repository for getting list of user wallets * [REDACTED_AUTHOR] */ class GetSelectedWalletSyncUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val useNewRepository: Boolean = false, ) { @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") operator fun invoke(): Either { - if (useNewRepository) { - return either { - userWalletsListRepository.selectedUserWallet.value ?: raise(GetUserWalletError.UserWalletNotFound) - } - } - return either { - ensureNotNull( - value = userWalletsListManager.selectedUserWalletSync, - raise = { GetUserWalletError.UserWalletNotFound }, - ) + userWalletsListRepository.selectedUserWallet.value ?: raise(GetUserWalletError.UserWalletNotFound) } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt index 2f62d3a115..b5506dacff 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSelectedWalletUseCase.kt @@ -4,7 +4,6 @@ import arrow.core.Either import arrow.core.raise.either import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filterNotNull @@ -12,36 +11,26 @@ import kotlinx.coroutines.flow.filterNotNull /** * Use case for getting flow of selected wallet. * - * @property userWalletsListManager user wallets list manager + * @property userWalletsListRepository repository for getting list of user wallets * [REDACTED_AUTHOR] */ @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") class GetSelectedWalletUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val useNewRepository: Boolean = false, ) { @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") operator fun invoke(): Either> { return either { - if (useNewRepository) { - userWalletsListRepository.selectedUserWallet.filterNotNull() - } else { - userWalletsListManager.selectedUserWallet - } + userWalletsListRepository.selectedUserWallet.filterNotNull() } } @Deprecated("You should provide the selected wallet via routing parameters due to the scalability of the features") fun sync(): Either { return either { - if (useNewRepository) { - userWalletsListRepository.selectedUserWallet.value - } else { - userWalletsListManager.selectedUserWalletSync - } + userWalletsListRepository.selectedUserWallet.value } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt index 8ca4292c70..577f7d8b93 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetUserWalletUseCase.kt @@ -10,24 +10,17 @@ import com.tangem.domain.common.wallets.requireUserWalletsSync import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.GetUserWalletError import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transformLatest class GetUserWalletUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val useNewListRepository: Boolean, ) { operator fun invoke(userWalletId: UserWalletId): Either = either { - val userWallets = if (useNewListRepository) { - userWalletsListRepository.requireUserWalletsSync() - } else { - userWalletsListManager.userWalletsSync - } + val userWallets = userWalletsListRepository.requireUserWalletsSync() ensureNotNull(userWallets.firstOrNull { it.walletId == userWalletId }) { raise(GetUserWalletError.UserWalletNotFound) @@ -36,11 +29,7 @@ class GetUserWalletUseCase( @OptIn(ExperimentalCoroutinesApi::class) fun invokeFlow(userWalletId: UserWalletId): EitherFlow { - val flow = if (useNewListRepository) { - userWalletsListRepository.userWallets.map { requireNotNull(it) } - } else { - userWalletsListManager.userWallets - } + val flow = userWalletsListRepository.userWallets.map { requireNotNull(it) } return flow.transformLatest { userWallets -> userWallets.firstOrNull { it.walletId == userWalletId } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt index 549644162e..d9a1ee4499 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletNamesUseCase.kt @@ -2,22 +2,15 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.requireUserWalletsSync -import com.tangem.domain.wallets.legacy.UserWalletsListManager /** * Use case for getting list of user wallets names. * - * @property userWalletsListManager user wallets list manager + * @property userWalletsListRepository repository for getting list of user wallets */ class GetWalletNamesUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val useNewRepository: Boolean, ) { - operator fun invoke(): List = if (useNewRepository) { - userWalletsListRepository.requireUserWalletsSync().map { it.name } - } else { - userWalletsListManager.userWalletsSync.map { it.name } - } + operator fun invoke(): List = userWalletsListRepository.requireUserWalletsSync().map { it.name } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsForAutomaticallyPushEnablingUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsForAutomaticallyPushEnablingUseCase.kt index 75c623d401..fc73252e8f 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsForAutomaticallyPushEnablingUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsForAutomaticallyPushEnablingUseCase.kt @@ -2,7 +2,6 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -11,24 +10,17 @@ import kotlinx.coroutines.withContext * * This use case filters out wallets that have already had push notifications automatically enabled * from the complete list of user wallets, returning only those wallets that still need to have * push notifications automatically enabled. - * @property userWalletsListManager Manager for user wallets list operations * @property userWalletsListRepository Repository for user wallets list operations * @property dispatchers Coroutine dispatcher provider for background operations */ class GetWalletsForAutomaticallyPushEnablingUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val shouldUseNewListRepository: Boolean, private val dispatchers: CoroutineDispatcherProvider, ) { suspend operator fun invoke(walletsListWherePushWasEnabled: List): List = withContext(dispatchers.default) { - val allLocalWallets = if (shouldUseNewListRepository) { - userWalletsListRepository.userWalletsSync().map { it.walletId } - } else { - userWalletsListManager.userWalletsSync.map { it.walletId } - } + val allLocalWallets = userWalletsListRepository.userWalletsSync().map { it.walletId } allLocalWallets - walletsListWherePushWasEnabled.toSet() } } \ No newline at end of file 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 d92a9a5555..de1b19acf0 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,34 +2,23 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.legacy.UserWalletsListManager import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map /** * Use case for getting list of user wallets * - * @property userWalletsListManager user wallets list manager + * @property userWalletsListRepository repository for getting list of user wallets * [REDACTED_AUTHOR] */ class GetWalletsUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val useNewListRepository: Boolean, ) { @Throws(IllegalArgumentException::class) - operator fun invoke(): Flow> = if (useNewListRepository) { - userWalletsListRepository.userWallets.map { requireNotNull(it) } - } else { - userWalletsListManager.userWallets - } + operator fun invoke(): Flow> = userWalletsListRepository.userWallets.map { requireNotNull(it) } @Throws(IllegalArgumentException::class) - fun invokeSync(): List = if (useNewListRepository) { - userWalletsListRepository.userWallets.value!! - } else { - userWalletsListManager.userWalletsSync - } + fun invokeSync(): List = userWalletsListRepository.userWallets.value!! } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt index 25722fa134..e15cfc13df 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsNeedToBackupUseCase.kt @@ -4,27 +4,20 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.legacy.UserWalletsListManager import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map /** * Use case that checks if wallet need backup cards * - * @property userWalletsListManager user wallets list manager + * @property userWalletsListRepository repository for getting user wallets */ class IsNeedToBackupUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val useNewRepository: Boolean, ) { operator fun invoke(id: UserWalletId): Flow { - val userWalletsFlow = if (useNewRepository) { - userWalletsListRepository.userWallets - } else { - userWalletsListManager.userWallets - } + val userWalletsFlow = userWalletsListRepository.userWallets return userWalletsFlow .map { wallets -> diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsWalletAlreadySavedUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsWalletAlreadySavedUseCase.kt index 86cf920cd5..3f6b9ede87 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsWalletAlreadySavedUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/IsWalletAlreadySavedUseCase.kt @@ -5,28 +5,16 @@ import arrow.core.raise.either import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.error.SaveWalletError import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.legacy.UserWalletsListManager class IsWalletAlreadySavedUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val useNewRepository: Boolean, ) { suspend operator fun invoke( userWallet: UserWallet, canOverride: Boolean = false, - ): Either { - return if (useNewRepository) { - either { - userWalletsListRepository.userWalletsSync() - .any { it.walletId == userWallet.walletId } - } - } else { - either { - userWalletsListManager.userWalletsSync - .any { it.walletId == userWallet.walletId } - } - } + ): Either = either { + userWalletsListRepository.userWalletsSync() + .any { it.walletId == userWallet.walletId } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt index 354d0b6bc8..77f03f306a 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SaveWalletUseCase.kt @@ -1,78 +1,52 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either -import arrow.core.left import arrow.core.raise.either import arrow.core.right -import com.tangem.common.doOnFailure -import com.tangem.common.doOnSuccess import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.error.SaveWalletError import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.legacy.UserWalletsListError -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository /** * Use case for saving user wallet * - * @property userWalletsListManager user wallets list manager - * [REDACTED_AUTHOR] */ class SaveWalletUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, private val walletsRepository: WalletsRepository, - private val useNewRepository: Boolean, ) { suspend operator fun invoke(userWallet: UserWallet, canOverride: Boolean = false): Either { - return if (useNewRepository) { - either { - val newUserWallet = - userWalletsListRepository.userWalletsSync().none { it.walletId == userWallet.walletId } - val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride).bind() + return either { + val newUserWallet = + userWalletsListRepository.userWalletsSync().none { it.walletId == userWallet.walletId } + val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride).bind() - if (newUserWallet) { - when (userWallet) { - is UserWallet.Cold -> { - if (walletsRepository.useBiometricAuthentication()) { - userWalletsListRepository.setLock( - userWallet.walletId, - UserWalletsListRepository.LockMethod.Biometric, - ) - } else { - Unit.right() - } - } - is UserWallet.Hot -> { + if (newUserWallet) { + when (userWallet) { + is UserWallet.Cold -> { + if (walletsRepository.useBiometricAuthentication()) { userWalletsListRepository.setLock( userWallet.walletId, - UserWalletsListRepository.LockMethod.NoLock, + UserWalletsListRepository.LockMethod.Biometric, ) + } else { + Unit.right() } - }.mapLeft { - SaveWalletError.DataError(null) - }.map { - userWalletsListRepository.select(userWallet.walletId) - }.bind() - } - } - } else { - either { - userWalletsListManager.save(userWallet, canOverride) - .doOnSuccess { return Unit.right() } - .doOnFailure { - return when (it) { - is UserWalletsListError.WalletAlreadySaved -> SaveWalletError.WalletAlreadySaved( - it.messageResId, - ) - else -> SaveWalletError.DataError(it.messageResId) - }.left() } - - return Unit.right() + is UserWallet.Hot -> { + userWalletsListRepository.setLock( + userWallet.walletId, + UserWalletsListRepository.LockMethod.NoLock, + ) + } + }.mapLeft { + SaveWalletError.DataError(null) + }.map { + userWalletsListRepository.select(userWallet.walletId) + }.bind() } } } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt index 8d416c90e3..239ada3255 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SelectWalletUseCase.kt @@ -1,47 +1,29 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either -import arrow.core.raise.either -import arrow.core.right -import com.tangem.common.CompletionResult import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.error.SelectWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.wallets.legacy.UserWalletsListManager /** * Use case for selecting wallet * - * @property userWalletsListManager user wallets list manager + * @property userWalletsListRepository repository for getting list of user wallets * @property reduxStateHolder redux state holder * [REDACTED_AUTHOR] */ class SelectWalletUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val useNewRepository: Boolean, private val reduxStateHolder: ReduxStateHolder, ) { suspend operator fun invoke(userWalletId: UserWalletId): Either { - if (useNewRepository) { - return userWalletsListRepository.select(userWalletId).map { - reduxStateHolder.onUserWalletSelected(it) - it - } - } - - return either { - return when (val result = userWalletsListManager.select(userWalletId)) { - is CompletionResult.Failure -> raise(SelectWalletError.UnableToSelectUserWallet) - is CompletionResult.Success -> { - reduxStateHolder.onUserWalletSelected(result.data) - result.data.right() - } - } + return userWalletsListRepository.select(userWalletId).map { + reduxStateHolder.onUserWalletSelected(it) + it } } } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsSyncUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsSyncUseCase.kt deleted file mode 100644 index 85e7b0265f..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsSyncUseCase.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.domain.wallets.usecase - -import com.tangem.domain.wallets.repository.WalletsRepository - -@Deprecated("Hot wallet feature makes app always save user wallets. Do not use this method") -class ShouldSaveUserWalletsSyncUseCase(private val walletsRepository: WalletsRepository) { - - suspend operator fun invoke(): Boolean = walletsRepository.shouldSaveUserWalletsSync() -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsUseCase.kt deleted file mode 100644 index fc62813050..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ShouldSaveUserWalletsUseCase.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.domain.wallets.usecase - -import com.tangem.domain.wallets.repository.WalletsRepository -import kotlinx.coroutines.flow.Flow - -class ShouldSaveUserWalletsUseCase(private val walletsRepository: WalletsRepository) { - - operator fun invoke(): Flow = walletsRepository.shouldSaveUserWallets() -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt deleted file mode 100644 index 3bbed19358..0000000000 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UnlockWalletsUseCase.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.domain.wallets.usecase - -import arrow.core.Either -import arrow.core.raise.either -import arrow.core.raise.ensureNotNull -import com.tangem.common.doOnFailure -import com.tangem.domain.wallets.legacy.UserWalletsListError -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.legacy.UserWalletsListManager.Lockable.UnlockType -import com.tangem.domain.wallets.legacy.asLockable -import com.tangem.domain.wallets.models.UnlockWalletsError - -/** - * Unlock wallets use case - * - * @property userWalletsListManager user wallets list manager - * -[REDACTED_AUTHOR] - */ -@Deprecated("Use NonBiometricUnlockWalletUseCase after migrating to new wallets repository") -class UnlockWalletsUseCase(private val userWalletsListManager: UserWalletsListManager) { - - suspend operator fun invoke(type: UnlockType = UnlockType.ANY): Either = either { - val userWalletsListManager = ensureNotNull( - value = userWalletsListManager.asLockable(), - raise = { - UnlockWalletsError.DataError( - cause = IllegalStateException("The lockable user wallets list manager could not be found"), - ) - }, - ) - - userWalletsListManager.unlock(type) - .doOnFailure { error -> - val e = when (error) { - is UserWalletsListError.NoUserWalletSelected -> UnlockWalletsError.NoUserWalletSelected - is UserWalletsListError.NotAllUserWalletsUnlocked -> - UnlockWalletsError.NotAllUserWalletsUnlocked - else -> UnlockWalletsError.UnableToUnlockWallets - } - - raise(e) - } - } -} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt index b8204b14fa..c06851d5d4 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/UpdateWalletUseCase.kt @@ -1,55 +1,41 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either -import arrow.core.raise.either -import com.tangem.common.CompletionResult import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.error.SaveWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UpdateWalletError import com.tangem.domain.wallets.models.UpdateWalletError.DataError /** * Use case for updating user wallet * - * @property userWalletsListManager user wallets list manager + * @property userWalletsListRepository repository for getting list of user wallets * [REDACTED_AUTHOR] */ class UpdateWalletUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val useNewRepository: Boolean, ) { suspend operator fun invoke( userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet, ): Either { - if (useNewRepository) { - val userWallet = userWalletsListRepository.userWallets.value?.find { it.walletId == userWalletId } - ?: return Either.Left( - DataError(IllegalStateException("User wallet with id $userWalletId not found")), - ) - val updatedWallet = update(userWallet) - return userWalletsListRepository.saveWithoutLock(updatedWallet, canOverride = true) - .mapLeft { - when (it) { - is SaveWalletError.DataError -> DataError( - IllegalStateException("Failed to update wallet: ${it.messageId}"), - ) - is SaveWalletError.WalletAlreadySaved -> UpdateWalletError.NameAlreadyExists - } + val userWallet = userWalletsListRepository.userWallets.value?.find { it.walletId == userWalletId } + ?: return Either.Left( + DataError(IllegalStateException("User wallet with id $userWalletId not found")), + ) + val updatedWallet = update(userWallet) + return userWalletsListRepository.saveWithoutLock(updatedWallet, canOverride = true) + .mapLeft { + when (it) { + is SaveWalletError.DataError -> DataError( + IllegalStateException("Failed to update wallet: ${it.messageId}"), + ) + is SaveWalletError.WalletAlreadySaved -> UpdateWalletError.NameAlreadyExists } - } - - return either { - when (val result = userWalletsListManager.update(userWalletId, update)) { - is CompletionResult.Failure -> raise(DataError(result.error)) - is CompletionResult.Success -> result.data } - } } } \ No newline at end of file diff --git a/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt deleted file mode 100644 index 4df283dd5e..0000000000 --- a/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCaseTest.kt +++ /dev/null @@ -1,141 +0,0 @@ -package com.tangem.domain.wallets.usecase - -import com.google.common.truth.Truth.assertThat -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.legacy.asLockable -import com.tangem.domain.wallets.legacy.isLockedSync -import com.tangem.domain.models.wallet.UserWallet -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test - -class GetSavedWalletsCountUseCaseTest { - - private lateinit var useCase: GetSavedWalletsCountUseCase - private lateinit var userWalletsListManager: UserWalletsListManager - - @Before - fun setup() { - userWalletsListManager = mockk() - useCase = GetSavedWalletsCountUseCase( - userWalletsListManager, - userWalletsListRepository = mockk(), - useNewRepository = false, - ) - mockkStatic("com.tangem.domain.wallets.legacy.UserWalletsListManagerExtensionsKt") - } - - @Test - fun `GIVEN manager is not lockable WHEN invoke THEN return empty list`() = runTest { - // GIVEN - every { userWalletsListManager.isLockable } returns false - every { userWalletsListManager.savedWalletsCount } returns flowOf(0) - every { userWalletsListManager.userWallets } returns flowOf(emptyList()) - every { userWalletsListManager.userWalletsSync } returns emptyList() - every { userWalletsListManager.isLockedSync } returns false - every { userWalletsListManager.asLockable() } returns null - - // WHEN - val result = useCase().firstOrNull() - - // THEN - assertThat(result).isEmpty() - } - - @Test - fun `GIVEN manager is locked WHEN invoke THEN return empty list`() = runTest { - // GIVEN - val mockLockable = mockk() - every { userWalletsListManager.isLockable } returns true - every { userWalletsListManager.savedWalletsCount } returns flowOf(0) - every { userWalletsListManager.userWallets } returns flowOf(emptyList()) - every { userWalletsListManager.userWalletsSync } returns emptyList() - every { userWalletsListManager.isLockedSync } returns true - every { userWalletsListManager.asLockable() } returns mockLockable - - // WHEN - val result = useCase().firstOrNull() - - // THEN - assertThat(result).isEmpty() - } - - @Test - fun `GIVEN manager is not locked and has wallets WHEN invoke THEN return wallets list`() = runTest { - // GIVEN - val mockLockable = mockk() - val wallets = listOf(mockk(), mockk()) - every { userWalletsListManager.isLockable } returns true - every { userWalletsListManager.savedWalletsCount } returns flowOf(wallets.size) - every { userWalletsListManager.userWallets } returns flowOf(wallets) - every { userWalletsListManager.userWalletsSync } returns wallets - every { userWalletsListManager.isLockedSync } returns false - every { userWalletsListManager.asLockable() } returns mockLockable - - // WHEN - val result = useCase().firstOrNull() - - // THEN - assertThat(result).isEqualTo(wallets) - } - - @Test - fun `GIVEN manager is not lockable and savedWalletsCount is zero WHEN invoke THEN return empty list`() = runTest { - // GIVEN - every { userWalletsListManager.isLockable } returns false - every { userWalletsListManager.savedWalletsCount } returns flowOf(0) - every { userWalletsListManager.userWallets } returns flowOf(emptyList()) - every { userWalletsListManager.userWalletsSync } returns emptyList() - every { userWalletsListManager.isLockedSync } returns false - every { userWalletsListManager.asLockable() } returns null - - // WHEN - val result = useCase().firstOrNull() - - // THEN - assertThat(result).isEmpty() - } - - @Test - fun `GIVEN manager is lockable and locked and savedWalletsCount is zero WHEN invoke THEN return empty list`() = - runTest { - // GIVEN - val mockLockable = mockk() - every { userWalletsListManager.isLockable } returns true - every { userWalletsListManager.savedWalletsCount } returns flowOf(0) - every { userWalletsListManager.userWallets } returns flowOf(emptyList()) - every { userWalletsListManager.userWalletsSync } returns emptyList() - every { userWalletsListManager.isLockedSync } returns true - every { userWalletsListManager.asLockable() } returns mockLockable - - // WHEN - val result = useCase().firstOrNull() - - // THEN - assertThat(result).isEmpty() - } - - @Test - fun `GIVEN manager is lockable and unlocked and savedWalletsCount is zero WHEN invoke THEN return empty list`() = - runTest { - // GIVEN - val mockLockable = mockk() - every { userWalletsListManager.isLockable } returns true - every { userWalletsListManager.savedWalletsCount } returns flowOf(0) - every { userWalletsListManager.userWallets } returns flowOf(emptyList()) - every { userWalletsListManager.userWalletsSync } returns emptyList() - every { userWalletsListManager.isLockedSync } returns false - every { userWalletsListManager.asLockable() } returns mockLockable - - // WHEN - val result = useCase().firstOrNull() - - // THEN - assertThat(result).isEmpty() - } -} \ No newline at end of file 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 1f8093ed68..78cd72167e 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 @@ -16,12 +16,10 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.SetAskBiometryShownUseCase -import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.biometry.impl.ui.state.AskBiometryUM -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.delay @@ -40,7 +38,6 @@ internal class AskBiometryModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, private val setAskBiometryShownUseCase: SetAskBiometryShownUseCase, - private val settingsRepository: SettingsRepository, private val tangemSdkManager: TangemSdkManager, private val getSelectedWalletUseCase: GetSelectedWalletUseCase, private val walletsRepository: WalletsRepository, @@ -48,7 +45,6 @@ internal class AskBiometryModel @Inject constructor( private val settingsManager: SettingsManager, private val uiMessageSender: UiMessageSender, private val userWalletsListRepository: UserWalletsListRepository, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val params = paramsContainer.require() @@ -110,24 +106,13 @@ internal class AskBiometryModel @Inject constructor( } private suspend fun handleSuccessAllowing(userWallet: UserWallet) { - walletsRepository.saveShouldSaveUserWallets(item = true) - - if (hotWalletFeatureToggles.isHotWalletEnabled) { - walletsRepository.setUseBiometricAuthentication(value = true) - walletsRepository.setRequireAccessCode(value = false) - setBiometryLockForAllWallets() - if (userWallet is UserWallet.Cold) { - cardSdkConfigRepository.setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = userWallet.hasAccessCode, - ) - } - } else { - settingsRepository.setShouldSaveAccessCodes(value = true) - if (userWallet is UserWallet.Cold) { - cardSdkConfigRepository.setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = userWallet.hasAccessCode, - ) - } + walletsRepository.setUseBiometricAuthentication(value = true) + walletsRepository.setRequireAccessCode(value = false) + setBiometryLockForAllWallets() + if (userWallet is UserWallet.Cold) { + cardSdkConfigRepository.setAccessCodeRequestPolicy( + isBiometricsRequestPolicy = userWallet.hasAccessCode, + ) } if (_uiState.value.isBottomSheetVariant) { 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 0760ef5917..2183fc05a0 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 @@ -32,7 +32,6 @@ import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.createwalletstart.entity.CreateWalletStartUM -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.delay @@ -64,7 +63,6 @@ internal class CreateWalletStartModel @Inject constructor( private val urlOpener: UrlOpener, private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { private val params = paramsContainer.require() @@ -99,7 +97,6 @@ internal class CreateWalletStartModel @Inject constructor( onBackClick = { router.pop() }, onScanClick = ::onScanClick, isScanInProgress = false, - isHotWalletOptionVisible = hotWalletFeatureToggles.isHotWalletVisible, ) CreateWalletStartComponent.Mode.HotWallet -> CreateWalletStartUM( title = resourceReference(R.string.hw_mobile_wallet), diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/entity/CreateWalletStartUM.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/entity/CreateWalletStartUM.kt index babf146ab3..bce2d230b2 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/entity/CreateWalletStartUM.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/entity/CreateWalletStartUM.kt @@ -17,7 +17,6 @@ internal data class CreateWalletStartUM( val otherMethodClick: () -> Unit, val onScanClick: () -> Unit, val onBackClick: () -> Unit, - val isHotWalletOptionVisible: Boolean = true, ) { data class FeatureItem( val iconResId: Int, diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/ui/CreateWalletStartContent.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/ui/CreateWalletStartContent.kt index d88ad17317..3c4245ca54 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/ui/CreateWalletStartContent.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/ui/CreateWalletStartContent.kt @@ -6,7 +6,6 @@ import androidx.annotation.DrawableRes import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.layout.Arrangement import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -165,74 +164,72 @@ internal fun CreateWalletStartContent(state: CreateWalletStartUM, modifier: Modi text = state.primaryButtonText.resolveReference(), onClick = state.onPrimaryButtonClick, ) - if (state.isHotWalletOptionVisible) { - Row( + + Row( + modifier = Modifier + .padding( + start = 16.dp, + top = 24.dp, + end = 16.dp, + ), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + DashedGradientLine( modifier = Modifier + .weight(1f) + .height(16.dp), + ) + Text( + text = stringResourceSafe(R.string.common_or), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + DashedGradientLine( + modifier = Modifier + .weight(1f) + .height(16.dp) + .scale(scaleX = -1f, scaleY = 1f), + ) + } + + if (state.otherMethodDescription != null) { + Text( + modifier = Modifier + .fillMaxWidth() .padding( start = 16.dp, - top = 24.dp, + top = 16.dp, end = 16.dp, ), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - DashedGradientLine( - modifier = Modifier - .weight(1f) - .height(16.dp), - ) - Text( - text = stringResourceSafe(R.string.common_or), - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - ) - DashedGradientLine( - modifier = Modifier - .weight(1f) - .height(16.dp) - .scale(scaleX = -1f, scaleY = 1f), - ) - } + text = state.otherMethodDescription.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ) } - if (state.isHotWalletOptionVisible) { - if (state.otherMethodDescription != null) { - Text( - modifier = Modifier - .fillMaxWidth() - .padding( - start = 16.dp, - top = 16.dp, - end = 16.dp, - ), - text = state.otherMethodDescription.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - ) - } - Row( - modifier = Modifier - .wrapContentWidth() - .clickable { state.otherMethodClick() } - .padding( - horizontal = 16.dp, - vertical = 12.dp, - ), - horizontalArrangement = Arrangement.Center, - ) { - Text( - text = state.otherMethodTitle.resolveReference(), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - Icon( - painter = painterResource(id = R.drawable.ic_chevron_right_18x24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - } + Row( + modifier = Modifier + .wrapContentWidth() + .clickable { state.otherMethodClick() } + .padding( + horizontal = 16.dp, + vertical = 12.dp, + ), + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = state.otherMethodTitle.resolveReference(), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + Icon( + painter = painterResource(id = R.drawable.ic_chevron_right_18x24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) } }, minImageHeight = 160.dp, diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 3373105976..018768f225 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -21,9 +21,7 @@ internal class PreviewDetailsComponent : DetailsComponent { ItemsBuilder( router = DummyRouter(), hotWalletFeatureToggles = object : HotWalletFeatureToggles { - override val isHotWalletEnabled: Boolean = true override val isWalletCreationRestrictionEnabled: Boolean = true - override val isHotWalletVisible: Boolean = true }, ).buildAll( isWalletConnectAvailable = true, 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 4abde012d5..3b3784c0cc 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 @@ -3,7 +3,6 @@ package com.tangem.features.details.model import android.content.res.Resources import arrow.core.getOrElse import com.tangem.common.routing.AppRoute -import com.tangem.core.analytics.AppInstanceIdProvider import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic @@ -34,7 +33,6 @@ import com.tangem.features.details.entity.DetailsUM import com.tangem.features.details.entity.SelectEmailFeedbackTypeBS import com.tangem.features.details.utils.ItemsBuilder import com.tangem.features.details.utils.SocialsBuilder -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.version.AppVersionProvider import kotlinx.collections.immutable.ImmutableList @@ -59,7 +57,6 @@ internal class DetailsModel @Inject constructor( private val checkIsWalletConnectAvailableUseCase: CheckIsWalletConnectAvailableUseCase, private val router: Router, private val urlOpener: UrlOpener, - private val appInstanceIdProvider: AppInstanceIdProvider, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val appStateHolder: ReduxStateHolder, private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, @@ -67,7 +64,6 @@ internal class DetailsModel @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, override val dispatchers: CoroutineDispatcherProvider, private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val analyticsEventHandler: AnalyticsEventHandler, private val tangemPayEligibilityManager: TangemPayEligibilityManager, ) : Model() { @@ -229,14 +225,10 @@ internal class DetailsModel @Inject constructor( private fun onBuyClick() { modelScope.launch { - if (hotWalletFeatureToggles.isHotWalletEnabled) { - analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Settings)) - generateBuyTangemCardLinkUseCase - .invoke(GenerateBuyTangemCardLinkUseCase.Source.Settings).let { urlOpener.openUrl(it) } - } else { - // This is incorrect implementation of buy link generation, but it is left here - urlOpener.openUrl(buildBuyLink()) - } + analyticsEventHandler.send(Basic.ButtonBuy(source = AnalyticsParam.ScreensSources.Settings)) + + val url = generateBuyTangemCardLinkUseCase(GenerateBuyTangemCardLinkUseCase.Source.Settings) + urlOpener.openUrl(url) } } @@ -270,12 +262,6 @@ internal class DetailsModel @Inject constructor( private fun getAppVersion(): String = "${appVersionProvider.versionName} (${appVersionProvider.versionCode})" - private suspend fun buildBuyLink(): String { - return appInstanceIdProvider.getAppInstanceId()?.let { - "$BUY_TANGEM_URL&app_instance_id=$it" - } ?: BUY_TANGEM_URL - } - private companion object { val SYSTEM_LANGUAGE = runCatching { Resources.getSystem().configuration.locales[0].language }.getOrElse { "" } val APP_LANGUAGE = Locale.getDefault().language @@ -283,7 +269,5 @@ internal class DetailsModel @Inject constructor( "&utm_medium=app" + "&utm_campaign=users-$SYSTEM_LANGUAGE" + "&utm_content=devicelang-$APP_LANGUAGE" - - val BUY_TANGEM_URL = "https://buy.tangem.com/?$UTM_MARKS" } } \ No newline at end of file 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 d1a90f171f..78426bd036 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 @@ -13,7 +13,6 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.wallets.usecase.UnlockWalletUseCase import com.tangem.features.details.entity.UserWalletListUM import com.tangem.features.details.impl.R @@ -32,7 +31,6 @@ import javax.inject.Inject @ModelScoped internal class UserWalletListModel @Inject constructor( userWalletsFetcherFactory: UserWalletsFetcher.Factory, - shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val router: Router, private val messageSender: UiMessageSender, override val dispatchers: CoroutineDispatcherProvider, @@ -47,7 +45,7 @@ internal class UserWalletListModel @Inject constructor( messageSender = messageSender, onlyMultiCurrency = false, isAuthMode = false, - isClickableIfLocked = hotWalletFeatureToggles.isHotWalletEnabled, + isClickableIfLocked = true, onWalletClick = ::onWalletClicked, ) @@ -68,67 +66,48 @@ internal class UserWalletListModel @Inject constructor( combine( flow = userWalletsFlow, - flow2 = shouldSaveUserWalletsUseCase(), - flow3 = isWalletSavingInProgress, - ) { userWallets, shouldSaveUserWallets, isWalletSavingInProgress -> - updateState(userWallets, shouldSaveUserWallets, isWalletSavingInProgress) + flow2 = isWalletSavingInProgress, + ) { userWallets, isWalletSavingInProgress -> + updateState(userWallets, isWalletSavingInProgress) }.collect() } } - private fun updateState( - userWallets: ImmutableList, - shouldSaveUserWallets: Boolean, - isWalletSavingInProgress: Boolean, - ) = state.update { value -> - value.copy( - userWallets = userWallets, - isWalletSavingInProgress = isWalletSavingInProgress, - addNewWalletText = when { - shouldSaveUserWallets || hotWalletFeatureToggles.isHotWalletEnabled -> { - resourceReference(R.string.user_wallet_list_add_button) - } - else -> resourceReference(R.string.scan_card_settings_button) - }, - ) - } + private fun updateState(userWallets: ImmutableList, isWalletSavingInProgress: Boolean) = + state.update { value -> + value.copy( + userWallets = userWallets, + isWalletSavingInProgress = isWalletSavingInProgress, + addNewWalletText = resourceReference(R.string.user_wallet_list_add_button), + ) + } private fun onAddNewWalletClick() { - if (hotWalletFeatureToggles.isHotWalletEnabled) { - analyticsEventHandler.send(SignIn.ButtonAddWallet(AnalyticsParam.ScreensSources.Settings)) + analyticsEventHandler.send(SignIn.ButtonAddWallet(AnalyticsParam.ScreensSources.Settings)) - if (hotWalletFeatureToggles.isWalletCreationRestrictionEnabled) { - withProgress(isWalletSavingInProgress) { - userWalletSaver.scanAndSaveUserWallet(modelScope) - } - } else { - router.push(AppRoute.CreateWalletSelection) - } - } else { + if (hotWalletFeatureToggles.isWalletCreationRestrictionEnabled) { withProgress(isWalletSavingInProgress) { userWalletSaver.scanAndSaveUserWallet(modelScope) } + } else { + router.push(AppRoute.CreateWalletSelection) } } private fun onWalletClicked(userWalletId: UserWalletId) { - if (hotWalletFeatureToggles.isHotWalletEnabled) { - modelScope.launch { - unlockWalletUseCase(userWalletId) - .onRight { router.push(AppRoute.WalletSettings(userWalletId)) } - .onLeft { error -> - Timber.e("Failed to unlock wallet $userWalletId: $error") - error.handle( - onUserCancelled = {}, - isFromUnlockAll = false, - onAlreadyUnlocked = { router.push(AppRoute.WalletSettings(userWalletId)) }, - analyticsEventHandler = analyticsEventHandler, - showMessage = messageSender::send, - ) - } - } - } else { - router.push(AppRoute.WalletSettings(userWalletId)) + modelScope.launch { + unlockWalletUseCase(userWalletId) + .onRight { router.push(AppRoute.WalletSettings(userWalletId)) } + .onLeft { error -> + Timber.e("Failed to unlock wallet $userWalletId: $error") + error.handle( + onUserCancelled = {}, + isFromUnlockAll = false, + onAlreadyUnlocked = { router.push(AppRoute.WalletSettings(userWalletId)) }, + analyticsEventHandler = analyticsEventHandler, + showMessage = messageSender::send, + ) + } } } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt index 04d7e91736..7133bc0d73 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -24,7 +24,6 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase import com.tangem.features.details.impl.R import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -38,7 +37,6 @@ internal class UserWalletSaver @Inject constructor( private val scanCardProcessor: ScanCardProcessor, private val saveWalletUseCase: SaveWalletUseCase, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, - private val shouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, private val reduxStateHolder: ReduxStateHolder, private val messageSender: UiMessageSender, private val router: Router, @@ -79,11 +77,7 @@ internal class UserWalletSaver @Inject constructor( recover = { error -> when (error) { is SaveWalletError.WalletAlreadySaved -> { - if (shouldSaveUserWalletsSyncUseCase()) { - selectUserWallet() - } else { - router.popTo() - } + selectUserWallet() } is SaveWalletError.DataError -> { val messageRef = ensureNotNull(error.messageId?.let(::resourceReference)) { @@ -111,7 +105,7 @@ internal class UserWalletSaver @Inject constructor( ) } - private suspend fun Raise.createUserWallet(response: ScanResponse): UserWallet { + private fun Raise.createUserWallet(response: ScanResponse): UserWallet { val userWallet = coldUserWalletBuilderFactory.create(scanResponse = response).build() return ensureNotNull(userWallet) { Error.Unknown } diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/DefaultHomeComponent.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/DefaultHomeComponent.kt index 7c210b9af7..f1450a1f61 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/DefaultHomeComponent.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/DefaultHomeComponent.kt @@ -9,7 +9,6 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.home.api.HomeComponent import com.tangem.features.home.impl.model.HomeModel import com.tangem.features.home.impl.ui.Home -import com.tangem.features.hotwallet.HotWalletFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -17,7 +16,6 @@ import dagger.assisted.AssistedInject internal class DefaultHomeComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted params: HomeComponent.Params, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : HomeComponent, AppComponentContext by appComponentContext { private val model: HomeModel = getOrCreateModel(params) @@ -26,11 +24,7 @@ internal class DefaultHomeComponent @AssistedInject constructor( override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() - Home( - state = state, - modifier = modifier, - isV2StoriesEnabled = hotWalletFeatureToggles.isHotWalletEnabled, - ) + Home(state = state, modifier = modifier) } @AssistedFactory 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 e650912dc6..099a3e49c3 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 @@ -35,14 +35,12 @@ import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.home.api.HomeComponent import com.tangem.features.home.impl.ui.state.HomeUM import com.tangem.features.home.impl.ui.state.Stories import com.tangem.features.home.impl.ui.state.getRestrictedStories -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay @@ -70,8 +68,6 @@ internal class HomeModel @Inject constructor( private val saveWalletUseCase: SaveWalletUseCase, private val generateBuyTangemCardLinkUseCase: GenerateBuyTangemCardLinkUseCase, private val urlOpener: UrlOpener, - private val userWalletsListManager: UserWalletsListManager, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val userWalletsListRepository: UserWalletsListRepository, private val reduxStateHolder: ReduxStateHolder, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, @@ -216,7 +212,7 @@ internal class HomeModel @Inject constructor( currency = currency, batch = scanResponse.card.batchId, signInType = SignInType.Card, - walletsCount = getWalletsCount().toString(), + walletsCount = userWalletsListRepository.userWalletsSync().size.toString(), isImported = isImported, hasBackup = scanResponse.card.backupStatus?.isActive, ), @@ -224,14 +220,6 @@ internal class HomeModel @Inject constructor( } } - private suspend fun getWalletsCount(): Int { - return if (hotWalletFeatureToggles.isHotWalletEnabled) { - userWalletsListRepository.userWalletsSync().size - } else { - userWalletsListManager.walletsCount - } - } - private fun setLoading(isLoading: Boolean) { _uiState.update { it.copy(scanInProgress = isLoading) } } diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/Home.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/Home.kt index 7277504058..b245ef97b4 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/Home.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/Home.kt @@ -5,29 +5,18 @@ import androidx.compose.ui.Modifier import com.tangem.core.ui.components.SystemBarsIconsDisposable import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.utils.ChangeRootBackgroundColorEffect -import com.tangem.features.home.impl.ui.compose.StoriesScreen import com.tangem.features.home.impl.ui.compose.StoriesScreenV2 import com.tangem.features.home.impl.ui.state.HomeUM @Composable -internal fun Home(state: HomeUM, isV2StoriesEnabled: Boolean, modifier: Modifier = Modifier) { +internal fun Home(state: HomeUM, modifier: Modifier = Modifier) { SystemBarsIconsDisposable(darkIcons = false) - if (isV2StoriesEnabled) { - StoriesScreenV2( - modifier = modifier, - state = state, - onGetStartedClick = state.onGetStartedClick, - ) - } else { - StoriesScreen( - modifier = modifier, - state = state, - onScanButtonClick = state.onScanClick, - onShopButtonClick = state.onShopClick, - onSearchTokensClick = state.onSearchTokensClick, - ) - } + StoriesScreenV2( + modifier = modifier, + state = state, + onGetStartedClick = state.onGetStartedClick, + ) ChangeRootBackgroundColorEffect(TangemColorPalette.Black) } \ No newline at end of file diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreen.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreen.kt deleted file mode 100644 index 8eab486edb..0000000000 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/ui/compose/StoriesScreen.kt +++ /dev/null @@ -1,271 +0,0 @@ -@file:Suppress("MagicNumber") - -package com.tangem.features.home.impl.ui.compose - -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.gestures.detectTapGestures -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.ContentScale -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 -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.test.StoriesScreenTestTags -import com.tangem.features.home.impl.ui.compose.content.* -import com.tangem.features.home.impl.ui.compose.views.HomeButtons -import com.tangem.features.home.impl.ui.compose.views.SearchCurrenciesButton -import com.tangem.features.home.impl.ui.compose.views.StoriesProgressBar -import com.tangem.features.home.impl.ui.state.Stories -import com.tangem.core.ui.R -import com.tangem.features.home.impl.ui.state.HomeUM -import kotlin.math.max - -@Composable -internal fun StoriesScreen( - state: HomeUM, - onScanButtonClick: () -> Unit, - onShopButtonClick: () -> Unit, - onSearchTokensClick: () -> Unit, - modifier: Modifier = Modifier, -) { - var currentStory by remember { mutableStateOf(state.firstStory) } - val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory)) - - LaunchedEffect(currentStoryIndex) { - if (currentStoryIndex < 0) { - currentStory = state.firstStory - } - } - - val goToPreviousStory = remember(currentStory, currentStoryIndex) { - { currentStory = state.stories[max(0, currentStoryIndex - 1)] } - } - val goToNextStory = remember(currentStory, currentStoryIndex) { - { - currentStory = if (currentStoryIndex >= 0 && currentStoryIndex < state.stories.lastIndex) { - state.stories[currentStoryIndex + 1] - } else { - state.firstStory - } - } - } - - // todo refactor [REDACTED_TASK_KEY] - StoriesScreenContent( - modifier = modifier - .fillMaxSize() - .testTag(StoriesScreenTestTags.SCREEN_CONTAINER), - config = StoriesScreenContentConfig( - storiesSize = state.stories.lastIndex, - currentStoryIndex = currentStoryIndex, - currentStory = currentStory, - isScanInProgress = state.scanInProgress, - onGoToPreviousStory = goToPreviousStory, - onGoToNextStory = goToNextStory, - onSearchTokensClick = onSearchTokensClick, - onScanButtonClick = onScanButtonClick, - onShopButtonClick = onShopButtonClick, - ), - ) -} - -@Deprecated("Use StoriesContainer from core/ui") -@Suppress("LongMethod") -@Composable -private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: Modifier = Modifier) { - var isPressed by remember { mutableStateOf(value = false) } - - val isPaused = isPressed || config.isScanInProgress - val currentStoryDuration = config.currentStory.duration - - Box( - modifier = modifier.background(Color(0xFF010101)), - ) { - Row( - modifier = Modifier.fillMaxSize(), - ) { - Box( - Modifier - .weight(1f) - .fillMaxHeight() - .pointerInput(Unit) { - detectTapGestures( - onPress = { - val pressStartTime = System.currentTimeMillis() - isPressed = true - this.tryAwaitRelease() - val pressEndTime = System.currentTimeMillis() - val totalPressTime = pressEndTime - pressStartTime - if (totalPressTime < 200) config.onGoToPreviousStory() - isPressed = false - }, - ) - }, - ) - Box( - Modifier - .weight(1f) - .fillMaxHeight() - .pointerInput(Unit) { - detectTapGestures( - onPress = { - val pressStartTime = System.currentTimeMillis() - isPressed = true - this.tryAwaitRelease() - val pressEndTime = System.currentTimeMillis() - val totalPressTime = pressEndTime - pressStartTime - if (totalPressTime < 200) config.onGoToNextStory() - isPressed = false - }, - ) - }, - ) - } - - Column( - modifier = Modifier - .statusBarsPadding() - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - StoriesProgressBar( - steps = config.storiesSize, - currentStep = config.currentStoryIndex, - stepDuration = currentStoryDuration, - paused = isPaused, - onStepFinish = config.onGoToNextStory, - ) - Image( - painter = painterResource(id = R.drawable.ic_tangem_logo), - contentDescription = null, - contentScale = ContentScale.FillHeight, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing16, - top = TangemTheme.dimens.spacing16, - ) - .height(TangemTheme.dimens.size18) - .align(Alignment.Start), - ) - when (config.currentStory) { - Stories.TangemIntro -> FirstStoriesContent( - isPaused = isPaused, - duration = currentStoryDuration, - ) - Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet() - Stories.UltraSecureBackup -> StoriesUltraSecureBackup( - isPaused = isPaused, - stepDuration = currentStoryDuration, - ) - Stories.Currencies -> StoriesCurrencies(isPaused, currentStoryDuration) - Stories.Web3 -> StoriesWeb3(isPaused, currentStoryDuration) - Stories.WalletForEveryone -> StoriesWalletForEveryone(currentStoryDuration) - } - } - Column( - modifier = Modifier - .navigationBarsPadding() - .padding(bottom = TangemTheme.dimens.spacing16) - .padding(horizontal = TangemTheme.dimens.spacing16) - .align(Alignment.BottomCenter) - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - AnimatedVisibility( - visible = config.currentStory == Stories.Currencies, - enter = fadeIn(), - exit = fadeOut(), - ) { - SearchCurrenciesButton( - modifier = Modifier.fillMaxWidth(), - onClick = config.onSearchTokensClick, - ) - } - - HomeButtons( - modifier = Modifier.fillMaxWidth(), - btnScanStateInProgress = config.isScanInProgress, - onScanButtonClick = config.onScanButtonClick, - onShopButtonClick = config.onShopButtonClick, - ) - } - } -} - -private data class StoriesScreenContentConfig( - val storiesSize: Int, - val currentStoryIndex: Int, - val currentStory: Stories, - val isScanInProgress: Boolean, - val onGoToPreviousStory: () -> Unit = {}, - val onGoToNextStory: () -> Unit = {}, - val onSearchTokensClick: () -> Unit = {}, - val onScanButtonClick: () -> Unit = {}, - val onShopButtonClick: () -> Unit = {}, -) - -// region Preview -@Preview(showBackground = true, widthDp = 360) -@Composable -private fun StoriesScreenContentPreview( - @PreviewParameter(StoriesScreenContentConfigProvider::class) config: StoriesScreenContentConfig, -) { - TangemThemePreview { - StoriesScreenContent(config = config) - } -} - -private class StoriesScreenContentConfigProvider : CollectionPreviewParameterProvider( - collection = listOf( - StoriesScreenContentConfig( - storiesSize = 6, - currentStoryIndex = 0, - currentStory = Stories.TangemIntro, - isScanInProgress = true, - ), - StoriesScreenContentConfig( - storiesSize = 6, - currentStoryIndex = 1, - currentStory = Stories.RevolutionaryWallet, - isScanInProgress = false, - ), - StoriesScreenContentConfig( - storiesSize = 6, - currentStoryIndex = 2, - currentStory = Stories.UltraSecureBackup, - isScanInProgress = false, - ), - StoriesScreenContentConfig( - storiesSize = 6, - currentStoryIndex = 3, - currentStory = Stories.Currencies, - isScanInProgress = false, - ), - StoriesScreenContentConfig( - storiesSize = 6, - currentStoryIndex = 4, - currentStory = Stories.Web3, - isScanInProgress = false, - ), - StoriesScreenContentConfig( - storiesSize = 6, - currentStoryIndex = 5, - currentStory = Stories.WalletForEveryone, - isScanInProgress = false, - ), - ), -) -// endregion Preview \ No newline at end of file diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt index 02cc6b1973..551b32f7e5 100644 --- a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt @@ -1,7 +1,5 @@ package com.tangem.features.hotwallet interface HotWalletFeatureToggles { - val isHotWalletEnabled: Boolean val isWalletCreationRestrictionEnabled: Boolean - val isHotWalletVisible: Boolean } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt index 9659ed2c61..1eadc2342d 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt @@ -5,11 +5,7 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager internal class DefaultHotWalletFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : HotWalletFeatureToggles { - override val isHotWalletEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "HOT_WALLET_ENABLED") + override val isWalletCreationRestrictionEnabled: Boolean - get() = isHotWalletEnabled && - featureTogglesManager.isFeatureEnabled(name = "HOT_WALLET_CREATION_RESTRICTION_ENABLED") - override val isHotWalletVisible: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "HOT_WALLET_VISIBLE") + get() = featureTogglesManager.isFeatureEnabled(name = "HOT_WALLET_CREATION_RESTRICTION_ENABLED") } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/di/HotWalletFeatureModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/di/HotWalletFeatureModule.kt index feec2263d5..ccb6fb049a 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/di/HotWalletFeatureModule.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/di/HotWalletFeatureModule.kt @@ -3,9 +3,6 @@ package com.tangem.features.hotwallet.di import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.features.hotwallet.DefaultHotWalletFeatureToggles import com.tangem.features.hotwallet.HotWalletFeatureToggles -import com.tangem.features.hotwallet.MnemonicRepository -import com.tangem.features.hotwallet.common.repository.DefaultMnemonicRepository -import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,15 +15,7 @@ internal object HotWalletFeatureModule { @Provides @Singleton - fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): HotWalletFeatureToggles { + fun provideHotWalletFeatureToggles(featureTogglesManager: FeatureTogglesManager): HotWalletFeatureToggles { return DefaultHotWalletFeatureToggles(featureTogglesManager) } -} - -@Module -@InstallIn(SingletonComponent::class) -internal interface HotWalletFeatureModuleBinds { - @Binds - @Singleton - fun provideMnemonicRepository(repository: DefaultMnemonicRepository): MnemonicRepository } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/di/MnemonicRepositoryModule.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/di/MnemonicRepositoryModule.kt new file mode 100644 index 0000000000..3b46484973 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/di/MnemonicRepositoryModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.hotwallet.di + +import com.tangem.features.hotwallet.MnemonicRepository +import com.tangem.features.hotwallet.common.repository.DefaultMnemonicRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface MnemonicRepositoryModule { + + @Binds + @Singleton + fun provideMnemonicRepository(repository: DefaultMnemonicRepository): MnemonicRepository +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt index 6f3f8945e9..d3a679ae26 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/entry/impl/model/OnboardingEntryModel.kt @@ -17,10 +17,7 @@ import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.repositories.SettingsRepository -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.legacy.asLockable import com.tangem.features.biometry.AskBiometryComponent -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.onboarding.v2.TitleProvider import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent @@ -47,8 +44,6 @@ internal class OnboardingEntryModel @Inject constructor( private val settingsRepository: SettingsRepository, private val analyticsEventHandler: AnalyticsEventHandler, private val uiMessageSender: UiMessageSender, - private val userWalletsListManager: UserWalletsListManager, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val userWalletsListRepository: UserWalletsListRepository, ) : Model() { @@ -215,29 +210,12 @@ internal class OnboardingEntryModel @Inject constructor( } private fun exitComponentScreen() { - // new flow - if (hotWalletFeatureToggles.isHotWalletEnabled) { - modelScope.launch { - if (userWalletsListRepository.userWalletsSync().isEmpty()) { - router.replaceAll(AppRoute.Home()) - } else { - router.replaceAll(AppRoute.Wallet) - } - } - return - } - - // legacy flow - if (userWalletsListManager.hasUserWallets) { - val isLocked = runCatching { userWalletsListManager.asLockable()?.isLocked!! }.getOrElse { false } - - if (isLocked) { - router.replaceAll(AppRoute.Welcome()) + modelScope.launch { + if (userWalletsListRepository.userWalletsSync().isEmpty()) { + router.replaceAll(AppRoute.Home()) } else { router.replaceAll(AppRoute.Wallet) } - } else { - router.replaceAll(AppRoute.Home()) } } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt index 8162044ed2..ae1378d60d 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/WalletCardItemDelegate.kt @@ -6,49 +6,37 @@ import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase import com.tangem.feature.walletsettings.entity.DialogConfig import com.tangem.feature.walletsettings.entity.WalletSettingsItemUM import com.tangem.feature.walletsettings.impl.R -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.wallet.utils.UserWalletImageFetcher import com.tangem.operations.attestation.ArtworkSize import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map internal class WalletCardItemDelegate @AssistedInject constructor( - private val getShouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, private val walletImageFetcher: UserWalletImageFetcher, @Assisted private val dialogNavigation: SlotNavigation, @Assisted private val onUpgradeHotWalletClick: () -> Unit, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) { - fun cardItemFlow(wallet: UserWallet): Flow = combine( - flow = walletImageFetcher.walletImage(wallet, ArtworkSize.SMALL), - flow2 = if (!hotWalletFeatureToggles.isHotWalletEnabled) { - flow { emit(getShouldSaveUserWalletsSyncUseCase()) } - } else { - flowOf(true) - }, - transform = { imageState, isRenameAvailable -> + fun cardItemFlow(wallet: UserWallet): Flow { + return walletImageFetcher.walletImage(wallet, ArtworkSize.SMALL).map { imageState -> val walletName = wallet.name WalletSettingsItemUM.CardBlock( id = "wallet_name", title = resourceReference(id = R.string.user_wallet_list_rename_popup_placeholder), text = stringReference(walletName), - isEnabled = isRenameAvailable, + isEnabled = true, onClick = { openRenameWalletDialog(wallet) }, imageState = imageState, additionalBlock = buildUpgradeToHardwareWalletBlockOrNull(wallet), ) - }, - ) + } + } private fun buildUpgradeToHardwareWalletBlockOrNull(wallet: UserWallet): BlockUM? { return BlockUM( 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 8618fdb312..67c8c2384d 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 @@ -43,7 +43,6 @@ 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.feed.entry.featuretoggle.FeedFeatureToggle -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener @@ -88,9 +87,7 @@ internal class WalletModel @Inject constructor( private val notificationsRepository: NotificationsRepository, private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase, private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase, - private val shouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val userWalletsListRepository: UserWalletsListRepository, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase, @@ -210,49 +207,39 @@ internal class WalletModel @Inject constructor( } private fun trackScreenOpened() { - if (hotWalletFeatureToggles.isHotWalletEnabled) { - modelScope.launch { - userWalletsListRepository - .selectedUserWalletSync() - ?.let { selectedWallet -> - val hasMobileWallet = userWalletsListRepository.userWalletsSync() - .any { it is UserWallet.Hot } + modelScope.launch { + userWalletsListRepository + .selectedUserWalletSync() + ?.let { selectedWallet -> + val hasMobileWallet = userWalletsListRepository.userWalletsSync() + .any { it is UserWallet.Hot } - val accountsCount = if (isAccountsModeEnabledUseCase.invokeSync()) { - singleAccountListSupplier(selectedWallet.walletId) - .first() - .accounts - .size - } else { - null - } - val result = getAppThemeModeUseCase().firstOrNull() - val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM - analyticsEventsHandler.send( - WalletScreenAnalyticsEvent.MainScreen.ScreenOpened( - hasMobileWallet = hasMobileWallet, - accountsCount = accountsCount, - theme = theme.value, - isImported = selectedWallet.isImported(), - ), - ) + val accountsCount = if (isAccountsModeEnabledUseCase.invokeSync()) { + singleAccountListSupplier(selectedWallet.walletId) + .first() + .accounts + .size + } else { + null } - } - } else { - analyticsEventsHandler.send( - WalletScreenAnalyticsEvent.MainScreen.ScreenOpenedLegacy(), - ) + val result = getAppThemeModeUseCase().firstOrNull() + val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM + analyticsEventsHandler.send( + WalletScreenAnalyticsEvent.MainScreen.ScreenOpened( + hasMobileWallet = hasMobileWallet, + accountsCount = accountsCount, + theme = theme.value, + isImported = selectedWallet.isImported(), + ), + ) + } } } private suspend fun shouldShowAskBiometryBottomSheet(): Boolean { - return if (hotWalletFeatureToggles.isHotWalletEnabled) { - userWalletsListRepository.userWalletsSync().any { it is UserWallet.Cold } && - shouldShowAskBiometryUseCase() && - canUseBiometryUseCase() - } else { - innerWalletRouter.isWalletLastScreen() && shouldShowAskBiometryUseCase() && canUseBiometryUseCase() - } + return userWalletsListRepository.userWalletsSync().any { it is UserWallet.Cold } && + shouldShowAskBiometryUseCase() && + canUseBiometryUseCase() } private fun subscribeToUserWalletsUpdates() = channelFlow { @@ -298,20 +285,16 @@ internal class WalletModel @Inject constructor( modelScope.launch { val shouldAskNotificationPermissionsViaBs = notificationsRepository.shouldAskNotificationPermissionsViaBs() val shouldShow = notificationsRepository.shouldShowSubscribeOnNotificationsAfterUpdate() - val isBiometricsEnabled = shouldSaveUserWalletsSyncUseCase() val isHuaweiDevice = getIsHuaweiDeviceWithoutGoogleServicesUseCase() Timber.d( "push BS afterUpdate: $shouldShow," + - "isBiometricsEnabled $isBiometricsEnabled," + "isHuaweiDevice $isHuaweiDevice", ) if (!shouldAskNotificationPermissionsViaBs) { notificationsRepository.setShouldAskNotificationPermissionsViaBs(true) return@launch } - if (!hotWalletFeatureToggles.isHotWalletEnabled && !isBiometricsEnabled) { - return@launch - } + if (!shouldShow) { return@launch } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt index ba738c15b9..ee8831d35c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCardClickIntents.kt @@ -1,56 +1,23 @@ package com.tangem.feature.wallet.child.wallet.model.intents -import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.activate -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.card.DeleteSavedAccessCodesUseCase -import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.usecase.DeleteWalletUseCase -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen -import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore -import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent -import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject internal interface WalletCardClickIntents { fun onRenameBeforeConfirmationClick(userWalletId: UserWalletId) - - fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) - - fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) } -// TODO: Refactor -@Suppress("LongParameterList") @ModelScoped internal class WalletCardClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateController, - private val tokenListStore: MultiWalletTokenListStore, - private val walletEventSender: WalletEventSender, - private val walletScreenContentLoader: WalletScreenContentLoader, - private val getUserWalletUseCase: GetUserWalletUseCase, - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val deleteWalletUseCase: DeleteWalletUseCase, - private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase, private val analyticsEventHandler: AnalyticsEventHandler, - private val reduxStateHolder: ReduxStateHolder, - private val appRouter: AppRouter, - private val dispatchers: CoroutineDispatcherProvider, ) : BaseWalletClickIntents(), WalletCardClickIntents { override fun onRenameBeforeConfirmationClick(userWalletId: UserWalletId) { @@ -63,47 +30,4 @@ internal class WalletCardClickIntentsImplementor @Inject constructor( ), ) } - - override fun onDeleteBeforeConfirmationClick(userWalletId: UserWalletId) { - analyticsEventHandler.send(MainScreen.DeleteWalletTapped()) - - walletEventSender.send( - event = WalletEvent.ShowAlert( - state = WalletAlertState.RemoveWalletAlert( - onConfirmClick = { onDeleteAfterConfirmationClick(userWalletId) }, - ), - ), - ) - } - - override fun onDeleteAfterConfirmationClick(userWalletId: UserWalletId) { - modelScope.launch(dispatchers.main) { - walletScreenContentLoader.cancel(userWalletId) - tokenListStore.remove(userWalletId) - - val walletToDelete = getUserWalletUseCase(userWalletId).getOrNull() ?: return@launch - val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse { - Timber.e("Unable to delete user wallet: $it") - return@launch - } - - if (walletToDelete is UserWallet.Cold) { - deleteSavedAccessCodesUseCase(cardId = walletToDelete.cardId).onLeft { - Timber.e("Unable to delete user wallet access code: $it") - } - } - - if (hasUserWallets) { - val selectedWallet = getSelectedWalletSyncUseCase().getOrElse { - error("Unable to find selected wallet: $it") - } - - reduxStateHolder.onUserWalletSelected(selectedWallet) - } else { - tokenListStore.clear() - stateHolder.clear() - appRouter.replaceAll(AppRoute.Home()) - } - } - } } \ No newline at end of file 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 b9373e40e8..05ca37c44e 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 @@ -13,7 +13,6 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.StakingBalance 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.nft.analytics.NFTAnalyticsEvent import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase @@ -33,7 +32,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBot import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCurrencyActionsConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.take @@ -90,7 +88,6 @@ internal interface WalletContentClickIntents { internal class WalletContentClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateController, private val currencyActionsClickIntents: WalletCurrencyActionsClickIntentsImplementor, - private val walletWarningsClickIntents: WalletWarningsClickIntentsImplementor, private val onrampStatusFactory: OnrampStatusFactory, private val getUserWalletUseCase: GetUserWalletUseCase, private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, @@ -100,44 +97,13 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val walletEventSender: WalletEventSender, private val analyticsEventHandler: AnalyticsEventHandler, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val accountDependencies: AccountDependencies, private val yieldSupplySetShouldShowMainPromoUseCase: YieldSupplySetShouldShowMainPromoUseCase, private val tokenListAnalyticsSender: TokenListAnalyticsSender, ) : BaseWalletClickIntents(), WalletContentClickIntents { override fun onDetailsClick() { - if (hotWalletFeatureToggles.isHotWalletEnabled) { - router.openDetailsScreen(stateHolder.getSelectedWalletId()) - return - } - - // Will be removed after Hot Wallet release - modelScope.launch(dispatchers.main) { - val userWalletId = stateHolder.getSelectedWalletId() - val userWallet = getUserWalletUseCase(userWalletId).getOrElse { - Timber.e( - """ - Unable to get user wallet - |- ID: $userWalletId - |- Exception: $it - """.trimIndent(), - ) - - return@launch - } - - if (userWallet.isLocked) { - stateHolder.showBottomSheet( - WalletBottomSheetConfig.UnlockWallets( - onUnlockClick = walletWarningsClickIntents::onUnlockWalletClick, - onScanClick = walletWarningsClickIntents::onScanToUnlockWalletClick, - ), - ) - } else { - router.openDetailsScreen(stateHolder.getSelectedWalletId()) - } - } + router.openDetailsScreen(stateHolder.getSelectedWalletId()) } override fun onOrganizeTokensClick() { 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 ed19a09083..b4a257bd42 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 @@ -35,8 +35,6 @@ import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher 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.wallets.legacy.UserWalletsListManager.Lockable.UnlockType -import com.tangem.domain.wallets.models.UnlockWalletsError import com.tangem.domain.wallets.usecase.* import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent @@ -46,11 +44,8 @@ import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWall import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletError import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBottomSheetConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent -import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async @@ -71,12 +66,8 @@ internal interface WalletWarningsClickIntents { fun onOpenUnlockWalletsBottomSheetClick() - fun onUnlockWalletClick() - fun onUnlockVisaAccessClick() - fun onScanToUnlockWalletClick() - fun onLikeAppClick() fun onDislikeAppClick() @@ -117,7 +108,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val remindToRateAppLaterUseCase: RemindToRateAppLaterUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val scanCardToUnlockWalletClickHandler: ScanCardToUnlockWalletClickHandler, - private val unlockWalletsUseCase: UnlockWalletsUseCase, private val nonBiometricUnlockWalletUseCase: NonBiometricUnlockWalletUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, @@ -131,7 +121,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, private val stakingIdFactory: StakingIdFactory, private val appRouter: AppRouter, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val userWalletsListRepository: UserWalletsListRepository, private val setShouldShowNotificationUseCase: SetShouldShowNotificationUseCase, private val notificationsRepository: NotificationsRepository, @@ -189,43 +178,21 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( override fun onOpenUnlockWalletsBottomSheetClick() { analyticsEventHandler.send(MainScreen.WalletUnlockTapped()) - if (hotWalletFeatureToggles.isHotWalletEnabled) { - modelScope.launch { - userWalletsListRepository.unlockAllWallets() - .onLeft { - val selectedUserWalletId = stateHolder.getSelectedWalletId() - nonBiometricUnlockWalletUseCase(selectedUserWalletId) - .onLeft { error -> - error.handle( - onAlreadyUnlocked = {}, - onUserCancelled = {}, - analyticsEventHandler = analyticsEventHandler, - isFromUnlockAll = true, - showMessage = uiMessageSender::send, - ) - } - } - } - return - } - - // Will be removed after hot wallet release - stateHolder.showBottomSheet( - WalletBottomSheetConfig.UnlockWallets( - onUnlockClick = this::onUnlockWalletClick, - onScanClick = this::onScanToUnlockWalletClick, - ), - ) - } - - @Deprecated("Will be removed with hot wallet release") - override fun onUnlockWalletClick() { - analyticsEventHandler.send(MainScreen.UnlockAllWithBiometrics()) - - modelScope.launch(dispatchers.main) { - unlockWalletsUseCase(type = UnlockType.ALL_WITHOUT_SELECT) - .onRight { stateHolder.update(CloseBottomSheetTransformer(stateHolder.getSelectedWalletId())) } - .onLeft(::handleUnlockWalletsError) + modelScope.launch { + userWalletsListRepository.unlockAllWallets() + .onLeft { + val selectedUserWalletId = stateHolder.getSelectedWalletId() + nonBiometricUnlockWalletUseCase(selectedUserWalletId) + .onLeft { error -> + error.handle( + onAlreadyUnlocked = {}, + onUserCancelled = {}, + analyticsEventHandler = analyticsEventHandler, + isFromUnlockAll = true, + showMessage = uiMessageSender::send, + ) + } + } } } @@ -233,25 +200,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( openScanCardDialog() } - private fun handleUnlockWalletsError(error: UnlockWalletsError) { - val event = when (error) { - is UnlockWalletsError.DataError, - is UnlockWalletsError.UnableToUnlockWallets, - -> WalletEvent.ShowError(resourceReference(R.string.user_wallet_list_error_unable_to_unlock)) - is UnlockWalletsError.NoUserWalletSelected, - is UnlockWalletsError.NotAllUserWalletsUnlocked, - -> WalletEvent.ShowAlert(WalletAlertState.RescanWallets) - } - - walletEventSender.send(event) - } - - @Deprecated("Will be removed with hot wallet release") - override fun onScanToUnlockWalletClick() { - analyticsEventHandler.send(MainScreen.UnlockWithCardScan()) - openScanCardDialog() - } - private fun openScanCardDialog() { modelScope.launch(dispatchers.main) { scanCardToUnlockWalletClickHandler(walletId = stateHolder.getSelectedWalletId()) 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 064c5030cf..c6754b834f 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 @@ -2,14 +2,11 @@ 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.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import timber.log.Timber class WalletNameMigrationUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val useNewListRepository: Boolean, private val walletNamesMigrationRepository: WalletNamesMigrationRepository, ) { @@ -18,28 +15,15 @@ class WalletNameMigrationUseCase( return } - if (useNewListRepository) { - val wallets = userWalletsListRepository.userWalletsSync() - val existingNames: MutableSet = mutableSetOf() - wallets.forEach { - val defaultName = it.name - val suggestedWalletName = suggestedWalletName(defaultName, existingNames) - if (defaultName != suggestedWalletName) { - userWalletsListRepository.saveWithoutLock(it.copy(name = suggestedWalletName), canOverride = true) - } - Timber.tag("Migrated names").e(it.walletId.toString() + " " + suggestedWalletName) - } - } else { - val wallets = userWalletsListManager.userWalletsSync - val existingNames: MutableSet = mutableSetOf() - wallets.indices.forEach { i -> - val defaultName = wallets[i].name - val suggestedWalletName = suggestedWalletName(defaultName, existingNames) - if (defaultName != suggestedWalletName) { - userWalletsListManager.update(wallets[i].walletId) { it.copy(name = suggestedWalletName) } - } - Timber.tag("Migrated names").e(i.toString() + " " + suggestedWalletName) + val wallets = userWalletsListRepository.userWalletsSync() + val existingNames: MutableSet = mutableSetOf() + wallets.forEach { + val defaultName = it.name + val suggestedWalletName = suggestedWalletName(defaultName, existingNames) + if (defaultName != suggestedWalletName) { + userWalletsListRepository.saveWithoutLock(it.copy(name = suggestedWalletName), canOverride = true) } + Timber.tag("Migrated names").e(it.walletId.toString() + " " + suggestedWalletName) } walletNamesMigrationRepository.setMigrationDone() 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 020e48bbb2..c71fb95879 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 @@ -9,7 +9,6 @@ import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -21,7 +20,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.tangempay.TangemPayFeatureToggles @Suppress("LongParameterList") @@ -40,14 +38,12 @@ internal class MultiWalletContentLoader( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, private val walletsRepository: WalletsRepository, private val currenciesRepository: CurrenciesRepository, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, ) : WalletContentLoader(id = userWallet.walletId) { @@ -92,13 +88,6 @@ internal class MultiWalletContentLoader( getStoryContentUseCase = getStoryContentUseCase, ).let(::add) - WalletDropDownItemsSubscriber( - stateHolder = stateHolder, - shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, - clickIntents = clickIntents, - hotWalletFeatureToggles = hotWalletFeatureToggles, - ).let(::add) - if (tangemPayFeatureToggles.isTangemPayEnabled) { add(tangemPayMainSubscriberFactory.create(userWallet)) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 083c1cdd5b..525899d92e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -9,7 +9,6 @@ import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.tokens.ApplyTokenListSortingUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -21,7 +20,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.TangemPayMainSubscriber -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.tangempay.TangemPayFeatureToggles import javax.inject.Inject @@ -38,7 +36,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, private val walletsRepository: WalletsRepository, private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, @@ -46,7 +43,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, ) { @@ -65,13 +61,11 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( walletWarningsSingleEventSender = walletWarningsSingleEventSender, applyTokenListSortingUseCase = applyTokenListSortingUseCase, getStoryContentUseCase = getStoryContentUseCase, - shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, walletsRepository = walletsRepository, getNFTCollectionsUseCase = getNFTCollectionsUseCase, currenciesRepository = currenciesRepository, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - hotWalletFeatureToggles = hotWalletFeatureToggles, tangemPayFeatureToggles = tangemPayFeatureToggles, tangemPayMainSubscriberFactory = tangemPayMainSubscriberFactory, yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt index df43ae0292..fea4a02329 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt @@ -2,14 +2,12 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.tangempay.TangemPayFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -25,10 +23,8 @@ internal class MultiWalletContentLoaderV2 @AssistedInject constructor( private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, ) : WalletContentLoader(id = userWallet.walletId) { @@ -50,12 +46,6 @@ internal class MultiWalletContentLoaderV2 @AssistedInject constructor( stateHolder = stateController, getStoryContentUseCase = getStoryContentUseCase, ), - WalletDropDownItemsSubscriber( - stateHolder = stateController, - shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, - clickIntents = clickIntents, - hotWalletFeatureToggles = hotWalletFeatureToggles, - ), if (tangemPayFeatureToggles.isTangemPayEnabled) { tangemPayMainSubscriberFactory.create(userWallet) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt index e21e22a688..34c986c644 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt @@ -10,13 +10,11 @@ import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import com.tangem.features.hotwallet.HotWalletFeatureToggles @Suppress("LongParameterList") internal class SingleWalletContentLoader( @@ -33,10 +31,8 @@ internal class SingleWalletContentLoader( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, - private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List { @@ -63,12 +59,6 @@ internal class SingleWalletContentLoader( getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, ), - WalletDropDownItemsSubscriber( - stateHolder = stateHolder, - shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, - clickIntents = clickIntents, - hotWalletFeatureToggles = hotWalletFeatureToggles, - ), SingleWalletExpressStatusesSubscriber( userWallet = userWallet, stateHolder = stateHolder, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt index 641029ddb5..4340bf544b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt @@ -11,12 +11,10 @@ import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.features.hotwallet.HotWalletFeatureToggles import javax.inject.Inject @ModelScoped @@ -33,10 +31,8 @@ internal class SingleWalletContentLoaderFactory @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, - private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) { fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents, isRefresh: Boolean): WalletContentLoader { @@ -56,8 +52,6 @@ internal class SingleWalletContentLoaderFactory @Inject constructor( walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, getOnrampTransactionsUseCase = getOnrampTransactionsUseCase, onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase, - shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, - hotWalletFeatureToggles = hotWalletFeatureToggles, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderV2.kt index 2440b54dae..6c4711cb5f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderV2.kt @@ -8,7 +8,6 @@ import com.tangem.domain.onramp.GetOnrampTransactionsUseCase import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender @@ -16,7 +15,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarni import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -35,13 +33,11 @@ internal class SingleWalletContentLoaderV2 @AssistedInject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, - private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val accountDependencies: AccountDependencies, private val walletWithFundsChecker: WalletWithFundsChecker, private val dispatchers: CoroutineDispatcherProvider, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List = listOf( @@ -66,12 +62,6 @@ internal class SingleWalletContentLoaderV2 @AssistedInject constructor( getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, ), - WalletDropDownItemsSubscriber( - stateHolder = stateHolder, - shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, - clickIntents = clickIntents, - hotWalletFeatureToggles = hotWalletFeatureToggles, - ), SingleWalletExpressStatusesSubscriberV2( userWallet = userWallet, singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index b5c79b14b9..a43c2c130e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -4,7 +4,6 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -15,8 +14,10 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarnin import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import com.tangem.features.hotwallet.HotWalletFeatureToggles +import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletActionButtonsSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletWithTokenListSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber @Deprecated("Use SingleWalletWithTokenContentLoaderV2 instead") @Suppress("LongParameterList") @@ -31,11 +32,9 @@ internal class SingleWalletWithTokenContentLoader( private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, private val tokenListStore: MultiWalletTokenListStore, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) : WalletContentLoader(id = userWallet.walletId) { @@ -66,12 +65,6 @@ internal class SingleWalletWithTokenContentLoader( stateHolder = stateHolder, getStoryContentUseCase = getStoryContentUseCase, ).let(::add) - WalletDropDownItemsSubscriber( - stateHolder = stateHolder, - shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, - clickIntents = clickIntents, - hotWalletFeatureToggles = hotWalletFeatureToggles, - ).let(::add) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt index bcf4c535a4..a0fcbbd771 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt @@ -5,7 +5,6 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -16,7 +15,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarnin import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.features.hotwallet.HotWalletFeatureToggles import javax.inject.Inject // TODO: Refactor @@ -32,11 +30,9 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, ) { @@ -53,10 +49,8 @@ internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, walletWarningsSingleEventSender = walletWarningsSingleEventSender, getStoryContentUseCase = getStoryContentUseCase, - shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - hotWalletFeatureToggles = hotWalletFeatureToggles, yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderV2.kt index f230baa27b..f11999ca24 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderV2.kt @@ -1,14 +1,15 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import com.tangem.features.hotwallet.HotWalletFeatureToggles +import com.tangem.feature.wallet.presentation.wallet.subscribers.CheckWalletWithFundsSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletWithTokenSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -23,8 +24,6 @@ internal class SingleWalletWithTokenContentLoaderV2 @AssistedInject constructor( private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List = listOf( @@ -37,12 +36,6 @@ internal class SingleWalletWithTokenContentLoaderV2 @AssistedInject constructor( walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, walletWarningsSingleEventSender = walletWarningsSingleEventSender, ), - WalletDropDownItemsSubscriber( - stateHolder = stateController, - shouldSaveUserWalletsUseCase = shouldSaveUserWalletsUseCase, - clickIntents = clickIntents, - hotWalletFeatureToggles = hotWalletFeatureToggles, - ), checkWalletWithFundsSubscriberFactory.create(userWallet), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt index 3e34289d7f..3846271993 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt @@ -38,13 +38,6 @@ internal sealed interface WalletAlertState { override val onConfirmClick: (() -> Unit)?, ) : Basic() - data class RemoveWalletAlert(override val onConfirmClick: (() -> Unit)?) : Basic() { - override val title: TextReference? = null - override val message: TextReference = resourceReference(id = R.string.user_wallet_list_delete_prompt) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_delete) - override val isWarningConfirmButton: Boolean = true - } - data class VisaLimitsInfo( val totalLimit: String, val otherLimit: String, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWalletCardDropDownItemsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWalletCardDropDownItemsTransformer.kt deleted file mode 100644 index 84cccf33f8..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWalletCardDropDownItemsTransformer.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.transformers - -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.feature.wallet.child.wallet.model.intents.WalletCardClickIntents -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDropDownItems -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState -import com.tangem.features.hotwallet.HotWalletFeatureToggles -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList - -internal class SetWalletCardDropDownItemsTransformer( - private val dropdownEnabled: Boolean, - private val clickIntents: WalletCardClickIntents, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, -) : WalletScreenStateTransformer { - override fun transform(prevState: WalletScreenState): WalletScreenState { - return prevState.copy(wallets = prevState.wallets.map(::transformWalletState).toImmutableList()) - } - - private fun transformWalletState(prevState: WalletState): WalletState { - return when (prevState) { - is WalletState.MultiCurrency.Content -> prevState.copy( - walletCardState = prevState.walletCardState.copySealed( - dropDownItems = constructDropDownItems(prevState.walletCardState.id), - ), - ) - is WalletState.SingleCurrency.Content -> prevState.copy( - walletCardState = prevState.walletCardState.copySealed( - dropDownItems = constructDropDownItems(prevState.walletCardState.id), - ), - ) - is WalletState.MultiCurrency.Locked -> prevState.copy( - walletCardState = prevState.walletCardState.copySealed( - dropDownItems = constructDropDownItems(prevState.walletCardState.id), - ), - ) - is WalletState.SingleCurrency.Locked -> prevState.copy( - walletCardState = prevState.walletCardState.copySealed( - dropDownItems = constructDropDownItems(prevState.walletCardState.id), - ), - ) - } - } - - private fun constructDropDownItems(userWalletId: UserWalletId): ImmutableList { - return if (dropdownEnabled) { - buildList { - add( - WalletDropDownItems( - text = resourceReference(id = R.string.common_rename), - icon = R.drawable.ic_edit_24, - onClick = { clickIntents.onRenameBeforeConfirmationClick(userWalletId) }, - ), - ) - if (!hotWalletFeatureToggles.isHotWalletEnabled) { - add( - WalletDropDownItems( - text = resourceReference(id = R.string.common_delete), - icon = R.drawable.ic_trash_24, - onClick = { clickIntents.onDeleteBeforeConfirmationClick(userWalletId) }, - ), - ) - } - }.toImmutableList() - } else { - persistentListOf() - } - } -} \ 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 c7baf4f61d..b38da828a2 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 @@ -4,12 +4,17 @@ import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent.Companion import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +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 kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow @@ -41,13 +46,7 @@ internal class WalletLoadingStateFactory( private fun createLoadingHotWalletContent(userWallet: UserWallet.Hot): WalletState.MultiCurrency.Content { return WalletState.MultiCurrency.Content( pullToRefreshConfig = createPullToRefreshConfig(), - walletCardState = WalletCardState.Loading( - id = userWallet.walletId, - title = userWallet.name, - additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet), - imageResId = null, - dropDownItems = persistentListOf(), - ), + walletCardState = createLoadingWalletCardState(userWallet), buttons = createMultiWalletActions(userWallet), warnings = persistentListOf(), bottomSheetConfig = null, @@ -61,7 +60,7 @@ internal class WalletLoadingStateFactory( private fun createLoadingMultiCurrencyContent(userWallet: UserWallet.Cold): WalletState.MultiCurrency.Content { return WalletState.MultiCurrency.Content( pullToRefreshConfig = createPullToRefreshConfig(), - walletCardState = userWallet.toLoadingWalletCardState(), + walletCardState = createLoadingWalletCardState(userWallet), buttons = createMultiWalletActions(userWallet), warnings = persistentListOf(), bottomSheetConfig = null, @@ -76,7 +75,7 @@ internal class WalletLoadingStateFactory( val currencySymbol = userWallet.scanResponse.cardTypesResolver.getBlockchain().currency return WalletState.SingleCurrency.Content( pullToRefreshConfig = createPullToRefreshConfig(), - walletCardState = userWallet.toLoadingWalletCardState(), + walletCardState = createLoadingWalletCardState(userWallet), warnings = persistentListOf(), bottomSheetConfig = null, buttons = createDimmedButtons(), @@ -98,13 +97,21 @@ internal class WalletLoadingStateFactory( ) } - private fun UserWallet.Cold.toLoadingWalletCardState(): WalletCardState { + private fun createLoadingWalletCardState(userWallet: UserWallet): WalletCardState { return WalletCardState.Loading( - id = walletId, - title = name, - additionalInfo = if (isMultiCurrency) WalletAdditionalInfoFactory.resolve(wallet = this) else null, - imageResId = walletImageResolver.resolve(userWallet = this), - dropDownItems = persistentListOf(), + id = userWallet.walletId, + title = userWallet.name, + additionalInfo = if (!userWallet.isMultiCurrency) { + null + } else { + WalletAdditionalInfoFactory.resolve(wallet = userWallet) + }, + imageResId = if (userWallet is UserWallet.Cold) { + walletImageResolver.resolve(userWallet) + } else { + null + }, + dropDownItems = createDropDownItems(userWalletId = userWallet.walletId), ) } @@ -150,4 +157,14 @@ internal class WalletLoadingStateFactory( WalletManageButton.Sell(enabled = true, dimContent = true, onClick = {}), ) } + + private fun createDropDownItems(userWalletId: UserWalletId): ImmutableList { + return persistentListOf( + WalletDropDownItems( + text = resourceReference(id = R.string.common_rename), + icon = R.drawable.ic_edit_24, + onClick = { clickIntents.onRenameBeforeConfirmationClick(userWalletId) }, + ), + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletDropDownItemsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletDropDownItemsSubscriber.kt deleted file mode 100644 index bf8a6b47cb..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletDropDownItemsSubscriber.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWalletCardDropDownItemsTransformer -import com.tangem.features.hotwallet.HotWalletFeatureToggles -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.onEach - -internal class WalletDropDownItemsSubscriber( - private val stateHolder: WalletStateController, - private val shouldSaveUserWalletsUseCase: ShouldSaveUserWalletsUseCase, - private val clickIntents: WalletClickIntents, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, -) : WalletSubscriber() { - override fun create(coroutineScope: CoroutineScope): Flow<*> { - if (!hotWalletFeatureToggles.isHotWalletEnabled) { - return shouldSaveUserWalletsUseCase.invoke() - .distinctUntilChanged() - .onEach { shouldSaveUserWallets -> - stateHolder.update( - SetWalletCardDropDownItemsTransformer( - dropdownEnabled = shouldSaveUserWallets, - clickIntents = clickIntents, - hotWalletFeatureToggles = hotWalletFeatureToggles, - ), - ) - } - } else { - return flow { - stateHolder.update( - SetWalletCardDropDownItemsTransformer( - dropdownEnabled = true, - clickIntents = clickIntents, - hotWalletFeatureToggles = hotWalletFeatureToggles, - ), - ) - } - } - } -} \ No newline at end of file diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index 4179ffefe8..5712dbb7ff 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -255,7 +255,7 @@ internal class WelcomeModel @Inject constructor( return canUseBiometryUseCase() && walletsRepository.useBiometricAuthentication() } - suspend fun nonBiometricUnlockWallet(userWalletId: UserWalletId) { + private suspend fun nonBiometricUnlockWallet(userWalletId: UserWalletId) { nonBiometricUnlockWalletUseCase(userWalletId) .onRight { routedOut = true From 4962acd1ca048fdfff5a10b0a17f965878bd2b79 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 Jan 2026 13:12:04 +0300 Subject: [PATCH 003/106] Updated on 2026-08-14 --- .../common/TangemHoldToConfirmButton.kt | 18 ++++--- .../tangem/domain/models/wallet/UserWallet.kt | 3 +- .../state/StakingStateController.kt | 10 ++-- .../impl/presentation/state/StakingUiState.kt | 1 + .../SetButtonsStateTransformer.kt | 53 ++++++++++++------- 5 files changed, 54 insertions(+), 31 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemHoldToConfirmButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemHoldToConfirmButton.kt index 1eabf50efc..3ee514559a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemHoldToConfirmButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemHoldToConfirmButton.kt @@ -74,13 +74,14 @@ private const val DEFAULT_SCALE = 1f private const val SHAKE_AMPLITUDE_DP = 5f private const val SHAKE_DURATION_MS = 120 -private const val FADE_DURATION_MS = 150 +private const val FADE_DURATION_MS = 100 private const val BOUNCE_VELOCITY_MULTIPLIER = 1.25f -private const val HAPTIC_INITIAL_INTERVAL_MS = 200L -private const val HAPTIC_MIN_INTERVAL_MS = 30L +private const val HAPTIC_INITIAL_INTERVAL_MS = 50L +private const val HAPTIC_MIN_INTERVAL_MS = 15L private const val HAPTIC_HEARTBEAT_INTERVAL_MS = 100L +private const val HAPTIC_SUCCESS_DELAY_MS = 300L // Easing: smooth acceleration, gradually picking up speed private val AccelerateEasing = CubicBezierEasing( @@ -288,6 +289,7 @@ private fun Modifier.holdToConfirmGestures( state.isProgressVisible = true // Soft heartbeat on success + delay(HAPTIC_SUCCESS_DELAY_MS) performSuccessHapticFeedback(config.hapticManager) state.scaleProgress.animateTo( @@ -324,7 +326,7 @@ private suspend fun performAcceleratingHapticFeedback(hapticManager: HapticManag // Exponential decrease: interval decreases faster as progress increases val interval = (maxInterval * (1f - progress * progress) + minInterval).toLong() - hapticManager.perform(TangemHapticEffect.View.SegmentTick) + hapticManager.perform(TangemHapticEffect.View.ClockTick) delay(interval) } } @@ -333,18 +335,18 @@ private suspend fun performAcceleratingHapticFeedback(hapticManager: HapticManag * Performs strong heartbeat haptic feedback on release (two heavy clicks). */ private suspend fun performReleaseHapticFeedback(hapticManager: HapticManager) { - hapticManager.perform(TangemHapticEffect.OneTime.HeavyClick) + hapticManager.perform(TangemHapticEffect.View.Reject) delay(HAPTIC_HEARTBEAT_INTERVAL_MS) - hapticManager.perform(TangemHapticEffect.OneTime.HeavyClick) + hapticManager.perform(TangemHapticEffect.View.Reject) } /** * Performs soft heartbeat haptic feedback on success (two light clicks). */ private suspend fun performSuccessHapticFeedback(hapticManager: HapticManager) { - hapticManager.perform(TangemHapticEffect.OneTime.Click) + hapticManager.perform(TangemHapticEffect.View.Confirm) delay(HAPTIC_HEARTBEAT_INTERVAL_MS) - hapticManager.perform(TangemHapticEffect.OneTime.Click) + hapticManager.perform(TangemHapticEffect.View.Confirm) } private suspend fun CoroutineScope.handleReleaseAnimation( diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWallet.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWallet.kt index 5df91b67b1..976b735b4f 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWallet.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWallet.kt @@ -110,4 +110,5 @@ val UserWallet.isLocked is UserWallet.Hot -> isLocked } -inline val UserWallet.isHotWallet get() = this is UserWallet.Hot \ No newline at end of file +inline val UserWallet.isHotWallet get() = this is UserWallet.Hot +inline val UserWallet.isColdWallet get() = this is UserWallet.Cold \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index a9164b0dca..e8ee07c6e8 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -18,6 +18,8 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import com.tangem.core.decompose.di.ModelScoped +import com.tangem.domain.models.wallet.isColdWallet +import com.tangem.domain.models.wallet.isHotWallet import javax.inject.Inject @ModelScoped @@ -35,9 +37,10 @@ internal class StakingStateController @Inject constructor( private val titleTransformer = SetTitleTransformer fun initializeWithUserWallet(userWallet: UserWallet) { - mutableUiState.update { - it.copy( - showColdWalletInteractionIcon = userWallet is UserWallet.Cold, + mutableUiState.update { state -> + state.copy( + showColdWalletInteractionIcon = userWallet.isColdWallet, + shouldShowHoldToConfirmButton = userWallet.isHotWallet, ) } } @@ -98,6 +101,7 @@ internal class StakingStateController @Inject constructor( buttonsState = NavigationButtonsState.Empty, balanceState = null, showColdWalletInteractionIcon = true, + shouldShowHoldToConfirmButton = false, ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 1b6605e4ff..699729948f 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -43,6 +43,7 @@ internal data class StakingUiState( val event: StateEvent, val balanceState: BalanceState?, val showColdWalletInteractionIcon: Boolean, + val shouldShowHoldToConfirmButton: Boolean, ) { fun copyWrapped( 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 05f544c7bb..04a86dbec7 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 @@ -7,6 +7,7 @@ import com.tangem.core.navigation.url.UrlOpener 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 import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.utils.getPendingActionTitle @@ -45,15 +46,17 @@ internal class SetButtonsStateTransformer( val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED - val isIconVisible = isConfirmation && !isCompleted + val isHoldToConfirm = prevState.shouldShowHoldToConfirmButton && isConfirmation && !isCompleted + val isIconVisible = isConfirmation && !isCompleted && !isHoldToConfirm val isPrimaryButtonDisabled = prevState.isPrimaryButtonDisabled() return NavigationButton( - textReference = prevState.getButtonText(), + textReference = prevState.getButtonText(isHoldToConfirm), iconRes = R.drawable.ic_tangem_24.takeIf { prevState.showColdWalletInteractionIcon }, isDimmed = isPrimaryButtonDisabled, isIconVisible = isIconVisible, shouldShowProgress = isInProgress, isEnabled = prevState.isButtonEnabled(), + isHoldToConfirm = isHoldToConfirm, onClick = { if (isPrimaryButtonDisabled) { prevState.clickIntents.showPrimaryClickAlert() @@ -89,7 +92,7 @@ internal class SetButtonsStateTransformer( else -> true } - private fun StakingUiState.getButtonText(): TextReference { + private fun StakingUiState.getButtonText(isHoldToConfirm: Boolean): TextReference { return when (currentStep) { StakingStep.InitialInfo -> { val initialState = initialInfoState as? StakingStates.InitialInfoState.Data @@ -100,7 +103,7 @@ internal class SetButtonsStateTransformer( } } StakingStep.Success -> resourceReference(R.string.common_close) - StakingStep.Confirmation -> getConfirmationButtonText() + StakingStep.Confirmation -> getConfirmationButtonText(isHoldToConfirm) StakingStep.Validators -> resourceReference(R.string.common_continue) StakingStep.Amount, StakingStep.RestakeValidator, @@ -109,25 +112,37 @@ internal class SetButtonsStateTransformer( } } - private fun StakingUiState.getConfirmationButtonText(): TextReference { + private fun StakingUiState.getConfirmationButtonText(isHoldToConfirm: Boolean): TextReference { val confirmationState = confirmationState as? StakingStates.ConfirmationState.Data + ?: return resourceReference(R.string.common_close) val amountState = amountState as? AmountState.Data - return if (confirmationState != null && amountState != null) { - when (actionType) { - is StakingActionCommonType.Enter -> { - val amount = amountState.amountTextField.cryptoAmount.value.orZero() - if (confirmationState.isApprovalNeeded && confirmationState.allowance < amount) { - resourceReference(R.string.give_permission_title) - } else { - resourceReference(R.string.common_stake) - } - } - is StakingActionCommonType.Exit -> resourceReference(R.string.common_unstake) - is StakingActionCommonType.Pending -> confirmationState.pendingAction?.type.getPendingActionTitle() + ?: 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) } - } else { - resourceReference(R.string.common_close) } + + val baseText = getBaseActionText(confirmationState) + ?: return resourceReference(R.string.common_close) + + return baseText.wrapWithHoldToIf(isHoldToConfirm) + } + + private fun StakingUiState.getBaseActionText( + confirmationState: StakingStates.ConfirmationState.Data, + ): TextReference? = when (actionType) { + is StakingActionCommonType.Enter -> resourceReference(R.string.common_stake) + is StakingActionCommonType.Exit -> resourceReference(R.string.common_unstake) + is StakingActionCommonType.Pending -> confirmationState.pendingAction?.type.getPendingActionTitle() + } + + private fun TextReference.wrapWithHoldToIf(condition: Boolean): TextReference = if (condition) { + resourceReference(id = R.string.common_hold_to, formatArgs = wrappedList(this)) + } else { + this } private fun StakingUiState.onPrimaryClick() { From c7941ee1f7187d7ab8e3990c69cd6f65abbbf5db Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 14:53:43 +0500 Subject: [PATCH 004/106] Updated on 2026-08-14 --- .../com/tangem/screens/DialogPageObject.kt | 10 +++++++ .../com/tangem/screens/MarketsPageObject.kt | 28 ++++--------------- .../tests/balance/TotalBalanceUpdateTest.kt | 16 +++++++---- .../tangem/features/feed/ui/feed/FeedList.kt | 5 +++- 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt index 7f6402803d..39fc2fd1fc 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt @@ -45,6 +45,16 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : hasText(getResourceString(R.string.common_continue)) } + val addButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.common_add)) + } + + val laterButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.common_later)) + } + val okButton: KNode = child { hasTestTag(BaseButtonTestTags.BUTTON) hasText(getResourceString(R.string.common_ok)) diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MarketsPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MarketsPageObject.kt index cad5a0cdea..ff3a31ce16 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MarketsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MarketsPageObject.kt @@ -1,47 +1,29 @@ package com.tangem.screens -import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase import com.tangem.common.constants.TestConstants.MARKETS_MAIN_NETWORK_SUFFIX -import com.tangem.common.utils.LazyListItemNode import com.tangem.core.ui.test.BaseButtonTestTags import com.tangem.core.ui.test.MarketsTestTags import com.tangem.core.ui.test.TopAppBarTestTags -import com.tangem.core.ui.utils.LazyListItemPositionSemantics import com.tangem.features.onramp.impl.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.compose.node.element.lazylist.KLazyListNode import io.github.kakaocup.kakao.common.utilities.getResourceString -import androidx.compose.ui.test.hasText as withText class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { - private val lazyList = KLazyListNode( - semanticsProvider = semanticsProvider, - viewBuilderAction = { hasTestTag(MarketsTestTags.TOKENS_LIST) }, - itemTypeBuilder = { itemType(::LazyListItemNode) }, - positionMatcher = { position -> - SemanticsMatcher.expectValue( - LazyListItemPositionSemantics, - position - ) - } - ) - val addToPortfolioButton: KNode = child { hasTestTag(BaseButtonTestTags.TEXT) hasText(getResourceString(R.string.common_add_to_portfolio)) useUnmergedTree = true } - val mainNetworkSwitch: KNode = child { - hasAnyDescendant(withText(MARKETS_MAIN_NETWORK_SUFFIX)) - useUnmergedTree = true - }.child { hasTestTag(MarketsTestTags.ADD_TO_PORTFOLIO_SWITCH) } + val mainNetworkSuffix: KNode = child { + hasText(MARKETS_MAIN_NETWORK_SUFFIX) + } val topBarBackButton: KNode = child { hasTestTag(TopAppBarTestTags.CLOSE_BUTTON) @@ -54,9 +36,9 @@ class MarketsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : } fun tokenWithTitle(title: String): KNode { - return lazyList.child { + return child { + hasTestTag(MarketsTestTags.TOKENS_LIST_ITEM) hasText(title) - useUnmergedTree = true } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceUpdateTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceUpdateTest.kt index eca72183ce..60771fc2ea 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceUpdateTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceUpdateTest.kt @@ -57,7 +57,7 @@ class TotalBalanceUpdateTest : BaseTestCase() { val tokenTitle = "XRP" val scenarioName = "quotes_api" val scenarioState = "Ripple" - val updatedBalance = "$3,307.18" + val updatedBalance = "$3,320.47" setupHooks( additionalAfterSection = { resetWireMockScenarioState(scenarioName) @@ -85,14 +85,18 @@ class TotalBalanceUpdateTest : BaseTestCase() { step("Click on 'Add to portfolio' button") { onMarketsScreen { addToPortfolioButton.clickWithAssertion() } } - step("Toggle the main network switch") { - onMarketsScreen { mainNetworkSwitch.performClick() } + step("Click on main network") { + onMarketsScreen { mainNetworkSuffix.performClick() } } - step("Click on 'Continue' button") { - onDialog { continueButton.clickWithAssertion() } + step("Click on 'Add' button") { + onDialog { addButton.clickWithAssertion() } + onDialog { addButton.clickWithAssertion() } //TODO: [REDACTED_JIRA] } step("Assert 'Continue' is not displayed") { - onDialog { continueButton.assertIsNotDisplayed() } + onDialog { addButton.assertIsNotDisplayed() } + } + step("Click on 'Later' button") { + onDialog { laterButton.clickWithAssertion() } } step("Go back to 'Markets: tokens list'") { waitForIdle() 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 88ebc8dc1f..9b5b58f8d5 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 @@ -24,6 +24,7 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.onFirstVisible +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -52,6 +53,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe 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.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState import com.tangem.features.feed.ui.feed.state.* @@ -69,7 +71,8 @@ internal fun FeedListHeader( modifier = modifier .drawBehind { drawRect(background) } .padding(horizontal = 16.dp) - .padding(bottom = 8.dp), + .padding(bottom = 8.dp) + .testTag(SEARCH_BAR), ) } From ae7332f9555da970e8a4b8ea8a6fd2d0bc7d0d41 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 10:28:36 +0000 Subject: [PATCH 005/106] 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 d1e7a735d9..81757bf804 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.33-1407" +tangemBlockchainSdk = "develop-1391" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.33-576" +tangemCardSdk = "develop-573" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-541" +tangemHotSdk = "develop-539" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 2b327fd3e9887c5ddd353ec47db60da6bbb38e2a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 11:52:19 +0100 Subject: [PATCH 006/106] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../tangem/tap/di/domain/EarnDomainModule.kt | 48 ++++++ .../api/tangemTech/TangemTechApi.kt | 14 ++ .../models/EarnNetworkListResponse.kt | 14 ++ .../api/tangemTech/models/EarnResponse.kt | 33 ++++ data/earn/.gitignore | 1 + data/earn/build.gradle.kts | 44 +++++ .../data/earn/DefaultEarnErrorResolver.kt | 20 +++ .../data/earn/converter/EarnTokenConverter.kt | 21 +++ .../data/earn/datastore/EarnNetworksStore.kt | 13 ++ .../data/earn/datastore/EarnTopTokensStore.kt | 13 ++ .../com/tangem/data/earn/di/EarnDataModule.kt | 47 ++++++ .../earn/repository/DefaultEarnRepository.kt | 157 ++++++++++++++++++ .../batch/EarnTokensBatchFetcher.kt | 139 ++++++++++++++++ domain/earn/.gitignore | 1 + domain/earn/build.gradle.kts | 14 ++ .../tangem/domain/earn/EarnErrorResolver.kt | 8 + .../domain/earn/model/EarnTokensListConfig.kt | 17 ++ .../domain/earn/model/EarnTypealiases.kt | 9 + .../domain/earn/repository/EarnRepository.kt | 26 +++ .../earn/usecase/FetchEarnNetworksUseCase.kt | 15 ++ .../earn/usecase/FetchTopEarnTokensUseCase.kt | 21 +++ .../earn/usecase/GetEarnNetworksUseCase.kt | 58 +++++++ .../usecase/GetEarnTokensBatchFlowUseCase.kt | 24 +++ .../earn/usecase/GetTopEarnTokensUseCase.kt | 15 ++ .../tangem/domain/models/earn/EarnError.kt | 11 ++ .../tangem/domain/models/earn/EarnNetwork.kt | 14 ++ .../tangem/domain/models/earn/EarnNetworks.kt | 5 + .../tangem/domain/models/earn/EarnToken.kt | 26 +++ .../models/earn/EarnTokenWithCurrency.kt | 16 ++ .../tangem/domain/models/earn/EarnTopToken.kt | 5 + settings.gradle.kts | 2 + 32 files changed, 853 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnNetworkListResponse.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt create mode 100644 data/earn/.gitignore create mode 100644 data/earn/build.gradle.kts create mode 100644 data/earn/src/main/java/com/tangem/data/earn/DefaultEarnErrorResolver.kt create mode 100644 data/earn/src/main/java/com/tangem/data/earn/converter/EarnTokenConverter.kt create mode 100644 data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt create mode 100644 data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt create mode 100644 data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt create mode 100644 data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt create mode 100644 data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt create mode 100644 domain/earn/.gitignore create mode 100644 domain/earn/build.gradle.kts create mode 100644 domain/earn/src/main/java/com/tangem/domain/earn/EarnErrorResolver.kt create mode 100644 domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTokensListConfig.kt create mode 100644 domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTypealiases.kt create mode 100644 domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt create mode 100644 domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchEarnNetworksUseCase.kt create mode 100644 domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchTopEarnTokensUseCase.kt create mode 100644 domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt create mode 100644 domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnTokensBatchFlowUseCase.kt create mode 100644 domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetTopEarnTokensUseCase.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnError.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetwork.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetworks.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTokenWithCurrency.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTopToken.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e6002f64e0..a1514e89d5 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -167,6 +167,7 @@ dependencies { implementation(projects.domain.blockaid) implementation(projects.domain.hotWallet) implementation(projects.domain.news) + implementation(projects.domain.earn) implementation(projects.common) implementation(projects.common.routing) @@ -220,6 +221,7 @@ dependencies { implementation(projects.data.yieldSupply) implementation(projects.data.hotWallet) implementation(projects.data.news) + implementation(projects.data.earn) /** Features */ implementation(projects.features.referral.impl) diff --git a/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt new file mode 100644 index 0000000000..55a22513b5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt @@ -0,0 +1,48 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.earn.repository.EarnRepository +import com.tangem.domain.earn.usecase.* +import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +object EarnDomainModule { + + @Provides + fun provideFetchEarnNetworksUseCase(repository: EarnRepository): FetchEarnNetworksUseCase { + return FetchEarnNetworksUseCase(repository) + } + + @Provides + fun provideManageEarnNetworksUseCase( + earnRepository: EarnRepository, + userWalletsListRepository: UserWalletsListRepository, + multiNetworkStatusSupplier: MultiNetworkStatusSupplier, + ): GetEarnNetworksUseCase { + return GetEarnNetworksUseCase( + earnRepository = earnRepository, + userWalletsListRepository = userWalletsListRepository, + multiNetworkStatusSupplier = multiNetworkStatusSupplier, + ) + } + + @Provides + fun provideFetchTopEarnTokensUseCase(repository: EarnRepository): FetchTopEarnTokensUseCase { + return FetchTopEarnTokensUseCase(repository) + } + + @Provides + fun provideManageTopEarnTokensUseCase(repository: EarnRepository): GetTopEarnTokensUseCase { + return GetTopEarnTokensUseCase(repository) + } + + @Provides + fun provideGetEarnTokensBatchFlowUseCase(repository: EarnRepository): GetEarnTokensBatchFlowUseCase { + return GetEarnTokensBatchFlowUseCase(repository) + } +} \ No newline at end of file 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 a3b38cb1f0..9d61d0b413 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 @@ -198,4 +198,18 @@ interface TangemTechApi { */ @POST("v1/transaction-events") suspend fun transactionEvents(@Body name: TransactionEventBody): ApiResponse + + // region Earn + @GET("v1/earn/yield/markets") + suspend fun getEarnTokens( + @Query("isForEarn") isForEarn: Boolean?, + @Query("page") page: String? = null, + @Query("limit") limit: Int? = null, + @Query("type") type: String? = null, + @Query("network") network: String? = null, + ): ApiResponse + + @GET("v1/earn/networks") + suspend fun getEarnNetworks(@Query("type") type: String): ApiResponse + // endregion } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnNetworkListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnNetworkListResponse.kt new file mode 100644 index 0000000000..1d54fdaeb4 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnNetworkListResponse.kt @@ -0,0 +1,14 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class EarnNetworkListResponse( + @Json(name = "items") val items: List, +) + +@JsonClass(generateAdapter = true) +data class EarnNetworkResponse( + @Json(name = "networkId") val networkId: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt new file mode 100644 index 0000000000..0e48d4e0f4 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt @@ -0,0 +1,33 @@ +package com.tangem.datasource.api.tangemTech.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class EarnListResponse( + @Json(name = "meta") val meta: MetaEarnListResponse, + @Json(name = "items") val items: List, +) + +@JsonClass(generateAdapter = true) +data class EarnResponse( + @Json(name = "apy") val apy: String, + @Json(name = "networkId") val networkId: String, + @Json(name = "rewardType") val rewardType: String, + @Json(name = "type") val type: String, + @Json(name = "token") val token: EarnTokenResponse, +) + +@JsonClass(generateAdapter = true) +data class EarnTokenResponse( + @Json(name = "id") val id: String, + @Json(name = "symbol") val symbol: String, + @Json(name = "name") val name: String, + @Json(name = "address") val address: String, +) + +@JsonClass(generateAdapter = true) +data class MetaEarnListResponse( + @Json(name = "page") val page: Int, + @Json(name = "limit") val limit: Int, +) \ No newline at end of file diff --git a/data/earn/.gitignore b/data/earn/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/data/earn/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/earn/build.gradle.kts b/data/earn/build.gradle.kts new file mode 100644 index 0000000000..985868438d --- /dev/null +++ b/data/earn/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.earn" +} + +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.earn) + implementation(projects.domain.common) + implementation(projects.domain.account.status) + // endregion + + // region Project - Libs + implementation(projects.libs.blockchainSdk) + // endregion + + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // endregion + + // region Other libraries + implementation(deps.androidx.datastore) + implementation(deps.moshi.kotlin) + implementation(deps.timber) + implementation(tangemDeps.blockchain) + // endregion + +} \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/DefaultEarnErrorResolver.kt b/data/earn/src/main/java/com/tangem/data/earn/DefaultEarnErrorResolver.kt new file mode 100644 index 0000000000..1b1d416a64 --- /dev/null +++ b/data/earn/src/main/java/com/tangem/data/earn/DefaultEarnErrorResolver.kt @@ -0,0 +1,20 @@ +package com.tangem.data.earn + +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.domain.earn.EarnErrorResolver +import com.tangem.domain.models.earn.EarnError + +internal class DefaultEarnErrorResolver : EarnErrorResolver { + + override fun resolve(throwable: Throwable?): EarnError { + return when (throwable) { + is ApiResponseError.HttpException -> { + EarnError.HttpError( + code = throwable.code.numericCode, + message = throwable.message.orEmpty(), + ) + } + else -> EarnError.NotHttpError + } + } +} \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/converter/EarnTokenConverter.kt b/data/earn/src/main/java/com/tangem/data/earn/converter/EarnTokenConverter.kt new file mode 100644 index 0000000000..eb33d52cb2 --- /dev/null +++ b/data/earn/src/main/java/com/tangem/data/earn/converter/EarnTokenConverter.kt @@ -0,0 +1,21 @@ +package com.tangem.data.earn.converter + +import com.tangem.datasource.api.tangemTech.models.EarnResponse +import com.tangem.domain.models.earn.EarnToken +import com.tangem.utils.converter.Converter + +internal object EarnTokenConverter : Converter { + + override fun convert(value: EarnResponse): EarnToken { + return EarnToken( + apy = value.apy, + networkId = value.networkId, + rewardType = value.rewardType, + type = value.type, + tokenId = value.token.id, + tokenSymbol = value.token.symbol, + tokenName = value.token.name, + tokenAddress = value.token.address, + ) + } +} \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt new file mode 100644 index 0000000000..ccf38c8902 --- /dev/null +++ b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt @@ -0,0 +1,13 @@ +package com.tangem.data.earn.datastore + +import arrow.core.Either +import com.tangem.datasource.local.datastore.RuntimeStateStore +import com.tangem.domain.models.earn.EarnNetworks +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class EarnNetworksStore @Inject constructor() : + RuntimeStateStore by RuntimeStateStore( + defaultValue = Either.Right(emptyList()), + ) \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt new file mode 100644 index 0000000000..dded4c2c9b --- /dev/null +++ b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt @@ -0,0 +1,13 @@ +package com.tangem.data.earn.datastore + +import arrow.core.Either +import com.tangem.datasource.local.datastore.RuntimeStateStore +import com.tangem.domain.models.earn.EarnTopToken +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class EarnTopTokensStore @Inject constructor() : + RuntimeStateStore by RuntimeStateStore( + defaultValue = Either.Right(emptyList()), + ) \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt b/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt new file mode 100644 index 0000000000..9cecd0e002 --- /dev/null +++ b/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt @@ -0,0 +1,47 @@ +package com.tangem.data.earn.di + +import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.earn.DefaultEarnErrorResolver +import com.tangem.data.earn.repository.DefaultEarnRepository +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.data.earn.datastore.EarnNetworksStore +import com.tangem.data.earn.datastore.EarnTopTokensStore +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.earn.EarnErrorResolver +import com.tangem.domain.earn.repository.EarnRepository +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 EarnDataModule { + + @Provides + fun provideEarnErrorResolver(): EarnErrorResolver = DefaultEarnErrorResolver() + + @Provides + @Singleton + fun provideEarnRepository( + tangemTechApi: TangemTechApi, + dispatchers: CoroutineDispatcherProvider, + userWalletsListRepository: UserWalletsListRepository, + cryptoCurrencyFactory: CryptoCurrencyFactory, + earnNetworksStore: EarnNetworksStore, + earnTopTokensStore: EarnTopTokensStore, + earnErrorResolver: EarnErrorResolver, + ): EarnRepository { + return DefaultEarnRepository( + tangemTechApi = tangemTechApi, + dispatchers = dispatchers, + userWalletsListRepository = userWalletsListRepository, + cryptoCurrencyFactory = cryptoCurrencyFactory, + earnNetworksStore = earnNetworksStore, + earnTopTokensStore = earnTopTokensStore, + earnErrorResolver = earnErrorResolver, + ) + } +} \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt b/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt new file mode 100644 index 0000000000..98e48ffd57 --- /dev/null +++ b/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt @@ -0,0 +1,157 @@ +package com.tangem.data.earn.repository + +import arrow.core.Either +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.earn.converter.EarnTokenConverter +import com.tangem.data.earn.datastore.EarnNetworksStore +import com.tangem.data.earn.datastore.EarnTopTokensStore +import com.tangem.data.earn.repository.batch.EarnTokensBatchFetcher +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.EarnResponse +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.earn.EarnErrorResolver +import com.tangem.domain.earn.model.EarnTokensBatchFlow +import com.tangem.domain.earn.model.EarnTokensBatchingContext +import com.tangem.domain.earn.repository.EarnRepository +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.earn.EarnNetwork +import com.tangem.domain.models.earn.EarnNetworks +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.domain.models.earn.EarnTopToken +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.isLocked +import com.tangem.pagination.BatchListSource +import com.tangem.pagination.toBatchFlow +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching +import kotlinx.coroutines.flow.Flow + +@Suppress("LongParameterList") +internal class DefaultEarnRepository( + private val tangemTechApi: TangemTechApi, + private val dispatchers: CoroutineDispatcherProvider, + private val userWalletsListRepository: UserWalletsListRepository, + private val cryptoCurrencyFactory: CryptoCurrencyFactory, + private val earnNetworksStore: EarnNetworksStore, + private val earnTopTokensStore: EarnTopTokensStore, + private val earnErrorResolver: EarnErrorResolver, +) : EarnRepository { + + override fun getEarnTokensBatchFlow(context: EarnTokensBatchingContext, batchSize: Int): EarnTokensBatchFlow { + val batchFetcher = EarnTokensBatchFetcher( + tangemTechApi = tangemTechApi, + batchSize = batchSize, + userWalletsListRepository = userWalletsListRepository, + cryptoCurrencyFactory = cryptoCurrencyFactory, + ) + + return BatchListSource( + fetchDispatcher = dispatchers.io, + context = context, + generateNewKey = { keys -> keys.lastOrNull()?.inc() ?: INITIAL_BATCH_KEY }, + batchFetcher = batchFetcher, + ).toBatchFlow() + } + + override suspend fun fetchEarnNetworks(type: String) { + runCatching(dispatchers.io) { + val response = tangemTechApi.getEarnNetworks(type = type).getOrThrow() + val items = response.items.map { dto -> + EarnNetwork( + networkId = dto.networkId, + isAdded = false, + ) + } + earnNetworksStore.store(Either.Right(items)) + }.onFailure { error -> + val earnError = earnErrorResolver.resolve(error) + earnNetworksStore.store(Either.Left(earnError)) + } + } + + override fun observeEarnNetworks(): Flow { + return earnNetworksStore.get() + } + + override suspend fun fetchTopEarnTokens(limit: Int) { + runCatching(dispatchers.io) { + val response = tangemTechApi.getEarnTokens( + isForEarn = true, + limit = limit, + ).getOrThrow() + + val userWallet = userWalletsListRepository + .selectedUserWallet + .value + .takeIf { wallet -> wallet?.isLocked?.not() == true } + ?: return@runCatching + + val items = response.items.mapNotNull { dto -> + val earnToken = EarnTokenConverter.convert(dto) + createCryptoCurrencyForEarnToken( + cryptoCurrencyFactory = cryptoCurrencyFactory, + userWallet = userWallet, + earnToken = dto, + )?.let { cryptoCurrency -> + EarnTokenWithCurrency( + earnToken = earnToken, + cryptoCurrency = cryptoCurrency, + ) + } + } + earnTopTokensStore.store(Either.Right(items)) + }.onFailure { error -> + val earnError = earnErrorResolver.resolve(error) + earnTopTokensStore.store(Either.Left(earnError)) + } + } + + override fun observeTopEarnTokens(): Flow { + return earnTopTokensStore.get() + } + + companion object { + internal const val FIRST_PAGE = 1 + private const val INITIAL_BATCH_KEY = 0 + } +} + +/* +use this CryptoCurrency only for creating cryptoCurrencyIcon!! because accountIndex = DerivationIndex.Main and it +doesn't provide real all data. + */ +internal fun createCryptoCurrencyForEarnToken( + userWallet: UserWallet, + earnToken: EarnResponse, + cryptoCurrencyFactory: CryptoCurrencyFactory, +): CryptoCurrency? { + val blockchain = Blockchain.fromNetworkId(earnToken.networkId) ?: Blockchain.Unknown + return if (earnToken.token.address.isEmpty()) { + cryptoCurrencyFactory.createCoin( + blockchain = blockchain, + extraDerivationPath = null, + userWallet = userWallet, + accountIndex = DerivationIndex.Main, + ) + } else { + val network = cryptoCurrencyFactory.networkFactory.create( + blockchain = blockchain, + extraDerivationPath = null, + userWallet = userWallet, + accountIndex = DerivationIndex.Main, + ) ?: return null + + cryptoCurrencyFactory.createToken( + network = network, + rawId = CryptoCurrency.RawID(earnToken.token.id), + name = earnToken.token.name, + symbol = earnToken.token.symbol, + decimals = blockchain.decimals(), + contractAddress = earnToken.token.address, + ) + } +} \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt b/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt new file mode 100644 index 0000000000..0495ea6c6c --- /dev/null +++ b/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt @@ -0,0 +1,139 @@ +package com.tangem.data.earn.repository.batch + +import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.earn.converter.EarnTokenConverter +import com.tangem.data.earn.repository.DefaultEarnRepository.Companion.FIRST_PAGE +import com.tangem.data.earn.repository.createCryptoCurrencyForEarnToken +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.EarnResponse +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.earn.model.EarnTokensListConfig +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.pagination.BatchFetchResult +import com.tangem.pagination.exception.EndOfPaginationException +import com.tangem.pagination.fetcher.BatchFetcher +import com.tangem.utils.coroutines.runSuspendCatching +import kotlinx.coroutines.flow.MutableStateFlow + +internal class EarnTokensBatchFetcher( + private val tangemTechApi: TangemTechApi, + private val batchSize: Int, + private val userWalletsListRepository: UserWalletsListRepository, + private val cryptoCurrencyFactory: CryptoCurrencyFactory, +) : BatchFetcher> { + + private val state: MutableStateFlow = MutableStateFlow(null) + + override suspend fun fetchFirst( + requestParams: EarnTokensListConfig, + ): BatchFetchResult> { + return runSuspendCatching { + loadPage( + page = FIRST_PAGE, + params = requestParams, + limit = batchSize, + ) + }.fold( + onSuccess = { result -> + state.value = result.state + result.batchResult + }, + onFailure = { throwable -> BatchFetchResult.Error(throwable) }, + ) + } + + override suspend fun fetchNext( + overrideRequestParams: EarnTokensListConfig?, + lastResult: BatchFetchResult>, + ): BatchFetchResult> { + val currentState = state.value ?: return BatchFetchResult.Error( + IllegalStateException("fetchFirst must be called"), + ) + + if (lastResult is BatchFetchResult.Success && lastResult.last && overrideRequestParams == null) { + return BatchFetchResult.Error(EndOfPaginationException()) + } + + val params = overrideRequestParams ?: currentState.params + val shouldReset = overrideRequestParams != null && overrideRequestParams != currentState.params + val pageToLoad = if (shouldReset) FIRST_PAGE else currentState.nextPage + + return runSuspendCatching { + loadPage( + page = pageToLoad, + params = params, + limit = batchSize, + ) + }.fold( + onSuccess = { result -> + state.value = result.state + result.batchResult + }, + onFailure = { throwable -> BatchFetchResult.Error(throwable) }, + ) + } + + private suspend fun loadPage(page: Int, params: EarnTokensListConfig, limit: Int): PageLoadResult { + fun createEarnTokenWithCurrency(userWallet: UserWallet, dto: EarnResponse): EarnTokenWithCurrency? { + val earnToken = EarnTokenConverter.convert(dto) + val cryptoCurrency = createCryptoCurrencyForEarnToken( + cryptoCurrencyFactory = cryptoCurrencyFactory, + userWallet = userWallet, + earnToken = dto, + ) + + return cryptoCurrency?.let { + EarnTokenWithCurrency( + earnToken = earnToken, + cryptoCurrency = cryptoCurrency, + ) + } + } + + val response = tangemTechApi.getEarnTokens( + isForEarn = params.isForEarn, + page = page.toString(), + limit = limit, + type = params.type, + network = params.network, + ).getOrThrow() + + val userWallet = userWalletsListRepository.selectedUserWallet.value + + val items = if (userWallet == null) { + emptyList() + } else { + response.items.mapNotNull { dto -> + createEarnTokenWithCurrency(userWallet, dto) + } + } + + val isLast = items.size < limit + + val batchResult = BatchFetchResult.Success( + data = items, + empty = items.isEmpty(), + last = isLast, + ) + + return PageLoadResult( + batchResult = batchResult, + state = EarnTokensPaginationState( + nextPage = response.meta.page + 1, + params = params, + ), + ) + } + + private data class PageLoadResult( + val batchResult: BatchFetchResult.Success>, + val state: EarnTokensPaginationState, + ) + + private data class EarnTokensPaginationState( + val nextPage: Int, + val params: EarnTokensListConfig, + ) +} \ No newline at end of file diff --git a/domain/earn/.gitignore b/domain/earn/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/earn/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/earn/build.gradle.kts b/domain/earn/build.gradle.kts new file mode 100644 index 0000000000..f72d726fea --- /dev/null +++ b/domain/earn/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +dependencies { + api(projects.domain.core) + api(projects.domain.models) + api(projects.core.pagination) + implementation(projects.domain.common) + implementation(projects.domain.networks) + implementation(deps.kotlin.serialization) +} \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/EarnErrorResolver.kt b/domain/earn/src/main/java/com/tangem/domain/earn/EarnErrorResolver.kt new file mode 100644 index 0000000000..f2120a63b7 --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/EarnErrorResolver.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.earn + +import com.tangem.domain.models.earn.EarnError + +interface EarnErrorResolver { + + fun resolve(throwable: Throwable?): EarnError +} \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTokensListConfig.kt b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTokensListConfig.kt new file mode 100644 index 0000000000..ab150727ee --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTokensListConfig.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.earn.model + +import kotlinx.serialization.Serializable + +/** + * Config of request earn tokens. + * + * @param type of Earn (ex: yield, staking). + * @param network networkId. + * @param isForEarn flag to load mostly used tokens. + */ +@Serializable +data class EarnTokensListConfig( + val type: String, + val network: String, + val isForEarn: Boolean, +) \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTypealiases.kt b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTypealiases.kt new file mode 100644 index 0000000000..fb9d2cf309 --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTypealiases.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.earn.model + +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.pagination.BatchFlow +import com.tangem.pagination.BatchingContext + +typealias EarnTokensBatchingContext = BatchingContext + +typealias EarnTokensBatchFlow = BatchFlow, Nothing> \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt b/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt new file mode 100644 index 0000000000..16d4ad67cb --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.earn.repository + +import com.tangem.domain.earn.model.EarnTokensBatchFlow +import com.tangem.domain.earn.model.EarnTokensBatchingContext +import com.tangem.domain.models.earn.EarnNetworks +import com.tangem.domain.models.earn.EarnTopToken +import kotlinx.coroutines.flow.Flow + +interface EarnRepository { + + fun getEarnTokensBatchFlow(context: EarnTokensBatchingContext, batchSize: Int): EarnTokensBatchFlow + + /** + * Load all networks for Earn and store it in data store. + */ + suspend fun fetchEarnNetworks(type: String) + + fun observeEarnNetworks(): Flow + + /** + * Fetch top N earn tokens by isForEarn = true and hold it in runtime store. + */ + suspend fun fetchTopEarnTokens(limit: Int) + + fun observeTopEarnTokens(): Flow +} \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchEarnNetworksUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchEarnNetworksUseCase.kt new file mode 100644 index 0000000000..fda4a392d4 --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchEarnNetworksUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.earn.usecase + +import arrow.core.Either +import com.tangem.domain.earn.repository.EarnRepository + +class FetchEarnNetworksUseCase( + private val repository: EarnRepository, +) { + + suspend operator fun invoke(type: String): Either { + return Either.catch { + repository.fetchEarnNetworks(type = type) + } + } +} \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchTopEarnTokensUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchTopEarnTokensUseCase.kt new file mode 100644 index 0000000000..7d33437dc5 --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchTopEarnTokensUseCase.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.earn.usecase + +import arrow.core.Either +import com.tangem.domain.earn.repository.EarnRepository + +class FetchTopEarnTokensUseCase( + private val repository: EarnRepository, +) { + + suspend operator fun invoke(limit: Int = DEFAULT_LIMIT): Either { + return Either.catch { + repository.fetchTopEarnTokens( + limit = limit, + ) + } + } + + private companion object { + private const val DEFAULT_LIMIT = 5 + } +} \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt new file mode 100644 index 0000000000..c5e240bf47 --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt @@ -0,0 +1,58 @@ +package com.tangem.domain.earn.usecase + +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.earn.repository.EarnRepository +import com.tangem.domain.models.earn.EarnNetwork +import com.tangem.domain.models.earn.EarnNetworks +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.isLocked +import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.networks.multi.MultiNetworkStatusProducer +import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.* + +/** + * Observes earn networks with [EarnNetwork.isAdded] enriched from user's wallets + * via [multiNetworkStatusSupplier]. Single entry point for all/mine filtering. + */ +class GetEarnNetworksUseCase( + private val earnRepository: EarnRepository, + private val userWalletsListRepository: UserWalletsListRepository, + private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, +) { + + operator fun invoke(): Flow { + return combine( + earnRepository.observeEarnNetworks(), + observeMyNetworkIds(), + ) { earn, myNetworkIds -> + earn.map { earnNetworks -> + earnNetworks.map { network -> + network.copy(isAdded = network.networkId in myNetworkIds) + } + } + }.distinctUntilChanged() + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun observeMyNetworkIds(): Flow> { + return userWalletsListRepository.userWallets + .map { it.orEmpty() } + .flatMapLatest { wallets -> + val activeWallets = wallets + .filterNot(UserWallet::isLocked) + .filter(UserWallet::isMultiCurrency) + if (activeWallets.isEmpty()) { + flowOf(emptySet()) + } else { + val flows = activeWallets.map { wallet -> + multiNetworkStatusSupplier( + MultiNetworkStatusProducer.Params(userWalletId = wallet.walletId), + ).map { statuses -> statuses.map { it.network.backendId }.toSet() } + } + combine(flows) { arrays -> arrays.flatMap { it }.toSet() } + } + } + } +} \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnTokensBatchFlowUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnTokensBatchFlowUseCase.kt new file mode 100644 index 0000000000..6531eb854e --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnTokensBatchFlowUseCase.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.earn.usecase + +import com.tangem.domain.earn.model.EarnTokensBatchFlow +import com.tangem.domain.earn.model.EarnTokensBatchingContext +import com.tangem.domain.earn.repository.EarnRepository + +/** + * Returns BatchFlow of Earn-tokens. + */ +class GetEarnTokensBatchFlowUseCase( + private val repository: EarnRepository, +) { + + operator fun invoke(context: EarnTokensBatchingContext, batchSize: Int = DEFAULT_BATCH_SIZE): EarnTokensBatchFlow { + return repository.getEarnTokensBatchFlow( + context = context, + batchSize = batchSize, + ) + } + + private companion object { + private const val DEFAULT_BATCH_SIZE = 20 + } +} \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetTopEarnTokensUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetTopEarnTokensUseCase.kt new file mode 100644 index 0000000000..b4a54344f2 --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetTopEarnTokensUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.earn.usecase + +import com.tangem.domain.earn.repository.EarnRepository +import com.tangem.domain.models.earn.EarnTopToken +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged + +class GetTopEarnTokensUseCase( + private val repository: EarnRepository, +) { + + operator fun invoke(): Flow { + return repository.observeTopEarnTokens().distinctUntilChanged() + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnError.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnError.kt new file mode 100644 index 0000000000..d5106276e8 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnError.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.models.earn + +sealed interface EarnError { + + data class HttpError( + val code: Int, + val message: String, + ) : EarnError + + data object NotHttpError : EarnError +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetwork.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetwork.kt new file mode 100644 index 0000000000..04d3edbc79 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetwork.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.models.earn + +import kotlinx.serialization.Serializable + +/** + * Model for networks in Earn. + * @param networkId - id of network + * @param isAdded - is exists in any user's wallet + */ +@Serializable +data class EarnNetwork( + val networkId: String, + val isAdded: Boolean, +) \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetworks.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetworks.kt new file mode 100644 index 0000000000..95ffdf7fb7 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetworks.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.models.earn + +import arrow.core.Either + +typealias EarnNetworks = Either> \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt new file mode 100644 index 0000000000..a91581e8eb --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.models.earn + +import kotlinx.serialization.Serializable + +/** + * Model of earn token. + * @param apy - percent of earning + * @param networkId - id of network + * @param rewardType - apr or apy + * @param type - staking or yield + * @param tokenSymbol - symbol of token (ex. "ATOM") + * @param tokenName - name of token (ex. "Cosmos Hub") + * @param tokenId - id of token (ex. "cosmos") + * @param tokenAddress - address of contract + */ +@Serializable +data class EarnToken( + val apy: String, + val networkId: String, + val rewardType: String, + val type: String, + val tokenId: String, + val tokenSymbol: String, + val tokenName: String, + val tokenAddress: String, +) \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTokenWithCurrency.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTokenWithCurrency.kt new file mode 100644 index 0000000000..5111c260d4 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTokenWithCurrency.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.models.earn + +import com.tangem.domain.models.currency.CryptoCurrency +import kotlinx.serialization.Serializable + +/** + * Represents an earn token and its associated cryptocurrency. This model is only for main account. + * + * @property earnToken The earn token details. + * @property cryptoCurrency Use this [CryptoCurrency] only for creating the cryptoCurrencyIcon!!!! + */ +@Serializable +data class EarnTokenWithCurrency( + val earnToken: EarnToken, + val cryptoCurrency: CryptoCurrency, +) \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTopToken.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTopToken.kt new file mode 100644 index 0000000000..7dd62ff84b --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTopToken.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.models.earn + +import arrow.core.Either + +typealias EarnTopToken = Either> \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 83aadc8341..d71fbf5bea 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -370,6 +370,7 @@ include(":domain:wallet-manager:models") include(":domain:yield-supply") include(":domain:yield-supply:models") include(":domain:news") +include(":domain:earn") // endregion Domain modules // region Data modules @@ -406,4 +407,5 @@ include(":data:express") include(":data:wallet-manager") include(":data:yield-supply") include(":data:news") +include(":data:earn") // endregion Data modules \ No newline at end of file From 33d59bf54eeccc95b61d43e175cb9a0be6633a7c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 13:53:21 +0200 Subject: [PATCH 007/106] Updated on 2026-08-14 --- .../configuration/utils/VersionNameProvider.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/utils/VersionNameProvider.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/utils/VersionNameProvider.kt index 01e50a8273..2cf83b5072 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/utils/VersionNameProvider.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/utils/VersionNameProvider.kt @@ -60,9 +60,17 @@ internal class VersionNameProvider( val outputFile = project.rootProject.file("find-latest-release-branch.output") return try { - project.providers.exec { + val execResult = project.providers.exec { commandLine("sh", scriptPath.absolutePath, currentBranch) - }.standardOutput.asText.get() + isIgnoreExitValue = true + } + + val exitCode = execResult.result.get().exitValue + if (exitCode != 0) { + project.logger.warn("Script failed with exit code: $exitCode") + outputFile.delete() + return null + } if (!outputFile.exists()) { project.logger.warn("Script output file not found") From 76894a77a75625ff5c914493ff9ebb1beba06ddd Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 13:40:04 +0300 Subject: [PATCH 008/106] Updated on 2026-08-14 --- .../details/DefaultMarketsTokenDetailsComponent.kt | 1 + .../model/market/details/MarketsTokenDetailsModel.kt | 11 +++++++++++ .../details/analytics/MarketDetailsAnalyticsEvent.kt | 5 +++++ .../ui/market/detailed/MarketsTokenDetailsContent.kt | 6 ++++++ .../detailed/preview/MarketsTokenDetailsPreview.kt | 2 ++ .../ui/market/detailed/state/MarketsTokenDetailsUM.kt | 1 + 6 files changed, 26 insertions(+) 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 2a561c6ba5..c3fc430041 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 @@ -95,6 +95,7 @@ internal class DefaultMarketsTokenDetailsComponent( tokenName = state.tokenName, tokenPrice = state.priceText, backgroundColor = background, + onShareClick = state.onShareClick, ) } 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 9d3d1ecd4b..257c93ed3b 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 @@ -11,6 +11,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent @@ -83,6 +84,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( private val excludedBlockchains: ExcludedBlockchains, private val urlOpener: UrlOpener, private val getNewsUseCase: GetNewsUseCase, + private val shareManager: ShareManager, ) : Model() { private val quotesJob = JobHolder() @@ -247,6 +249,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( onFirstVisible = {}, onScroll = {}, ), + onShareClick = ::onShareClick, ), ) @@ -662,6 +665,13 @@ internal class MarketsTokenDetailsModel @Inject constructor( } } + private fun onShareClick() { + val tokenId = params.token.id.value + val shareUrl = "$CRYPTOCURRENCIES_BASE_URL$tokenId" + shareManager.shareText(shareUrl) + analyticsEventHandler.send(analyticsEventBuilder.shareClicked()) + } + private fun onListedOnClick(exchangesCount: Int) { modelScope.launch { analyticsEventHandler.send(analyticsEventBuilder.exchangesScreenOpened()) @@ -725,5 +735,6 @@ internal class MarketsTokenDetailsModel @Inject constructor( private companion object { const val QUOTES_UPDATE_INTERVAL_MILLIS = 60000L const val RELATED_NEWS_LIMIT = 10 + const val CRYPTOCURRENCIES_BASE_URL = "https://tangem.com/en/cryptocurrencies/" } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/analytics/MarketDetailsAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/analytics/MarketDetailsAnalyticsEvent.kt index 05b527c4ff..1ae3f86fab 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/analytics/MarketDetailsAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/analytics/MarketDetailsAnalyticsEvent.kt @@ -65,6 +65,11 @@ internal class MarketDetailsAnalyticsEvent( "Provider" to provider, ), ) + + fun shareClicked() = MarketDetailsAnalyticsEvent( + event = "Button - Share", + params = mapOf("Token" to token.symbol), + ) } enum class IntervalType(val source: String) { 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 60ea3e9f82..77c6da0b24 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 @@ -42,6 +42,7 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.markets.PriceChangeInterval import com.tangem.features.feed.impl.R import com.tangem.features.feed.ui.market.detailed.components.* +import com.tangem.core.ui.R as CoreR import com.tangem.features.feed.ui.market.detailed.preview.MarketsTokenDetailsPreview import com.tangem.features.feed.ui.market.detailed.state.ExchangesBottomSheetContent import com.tangem.features.feed.ui.market.detailed.state.InfoBottomSheetContent @@ -150,6 +151,7 @@ internal fun MarketsTokenDetailsTopBar( tokenPrice: String, isBackButtonEnabled: Boolean, onBackClick: () -> Unit, + onShareClick: () -> Unit, ) { TangemTopAppBar( modifier = Modifier.drawBehind { drawRect(backgroundColor) }, @@ -159,6 +161,10 @@ internal fun MarketsTokenDetailsTopBar( onBackClicked = onBackClick, enabled = isBackButtonEnabled, ), + endButton = TopAppBarButtonUM.Icon( + iconRes = CoreR.drawable.ic_share_24, + onClicked = onShareClick, + ), ) } 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 2c578fbe68..70c4db0103 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 @@ -49,6 +49,7 @@ internal object MarketsTokenDetailsPreview { onFirstVisible = {}, onScroll = {}, ), + onShareClick = {}, ) val contentState = MarketsTokenDetailsUM( @@ -141,5 +142,6 @@ internal object MarketsTokenDetailsPreview { onFirstVisible = {}, onScroll = {}, ), + onShareClick = {}, ) } \ 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 e150d95894..d0c4775d82 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 @@ -28,6 +28,7 @@ internal data class MarketsTokenDetailsUM( val shouldShowPriceSubtitle: Boolean, val onShouldShowPriceSubtitleChange: (Boolean) -> Unit, val relatedNews: RelatedNews, + val onShareClick: () -> Unit, ) { data class ChartState( From 03c65b35cf0752982f0dc558b6a4dbda228fc65b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 18:18:37 +0500 Subject: [PATCH 009/106] Updated on 2026-08-14 --- .../com/tangem/tap/GoogleReviewManager.kt | 50 +++++++++++++++++ .../tap/di/GoogleReviewManagerModule.kt | 20 +++++++ .../tap/di/HuaweiReviewManagerModule.kt | 20 +++++++ .../navigation/review/DummyReviewManager.kt | 5 ++ .../core/navigation/review/ReviewManager.kt | 12 ++++ .../ScanCardToUnlockWalletClickHandler.kt | 56 ------------------- .../wallet/ui/utils/ReviewManagerRequester.kt | 47 ---------------- 7 files changed, 107 insertions(+), 103 deletions(-) create mode 100644 app/src/google/java/com/tangem/tap/GoogleReviewManager.kt create mode 100644 app/src/google/java/com/tangem/tap/di/GoogleReviewManagerModule.kt create mode 100644 app/src/huawei/java/com/tangem/tap/di/HuaweiReviewManagerModule.kt create mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/review/DummyReviewManager.kt create mode 100644 core/navigation/src/main/java/com/tangem/core/navigation/review/ReviewManager.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt diff --git a/app/src/google/java/com/tangem/tap/GoogleReviewManager.kt b/app/src/google/java/com/tangem/tap/GoogleReviewManager.kt new file mode 100644 index 0000000000..956e058583 --- /dev/null +++ b/app/src/google/java/com/tangem/tap/GoogleReviewManager.kt @@ -0,0 +1,50 @@ +package com.tangem.tap + +import android.app.Activity +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.google.android.play.core.review.ReviewManager as GReviewManager + +/** + * Implementation of [ReviewManager] for Google Play Store. + */ +internal class GoogleReviewManager : ReviewManager { + + override fun request(onDismissClick: () -> Unit) { + foregroundActivityObserver.withForegroundActivity { activity -> + val reviewManager = ReviewManagerFactory.create(activity) + val requestTask = reviewManager.requestReviewFlow() + requestTask + .addOnCompleteListener { task -> + handleOnCompleteRequestTask( + reviewManager = reviewManager, + activity = activity, + task = task, + onDismissClick = onDismissClick, + ) + } + .addOnFailureListener(Timber::e) + } + } + + private fun handleOnCompleteRequestTask( + reviewManager: GReviewManager, + activity: Activity, + task: Task, + onDismissClick: () -> Unit, + ) { + if (task.isSuccessful) { + val reviewFlow = reviewManager.launchReviewFlow(activity, task.result) + reviewFlow + .addOnCompleteListener { resultReviewTask -> + if (!resultReviewTask.isSuccessful) onDismissClick() + } + .addOnFailureListener(Timber::e) + } else { + Timber.e(task.exception) + } + } +} \ No newline at end of file diff --git a/app/src/google/java/com/tangem/tap/di/GoogleReviewManagerModule.kt b/app/src/google/java/com/tangem/tap/di/GoogleReviewManagerModule.kt new file mode 100644 index 0000000000..09f3a69e1d --- /dev/null +++ b/app/src/google/java/com/tangem/tap/di/GoogleReviewManagerModule.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.di + +import com.tangem.core.navigation.review.ReviewManager +import com.tangem.tap.GoogleReviewManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal class GoogleReviewManagerModule { + + @Singleton + @Provides + fun provideGoogleReviewManager(): ReviewManager { + return GoogleReviewManager() + } +} \ No newline at end of file diff --git a/app/src/huawei/java/com/tangem/tap/di/HuaweiReviewManagerModule.kt b/app/src/huawei/java/com/tangem/tap/di/HuaweiReviewManagerModule.kt new file mode 100644 index 0000000000..527ce2b716 --- /dev/null +++ b/app/src/huawei/java/com/tangem/tap/di/HuaweiReviewManagerModule.kt @@ -0,0 +1,20 @@ +package com.tangem.tap.di + +import com.tangem.core.navigation.review.DummyReviewManager +import com.tangem.core.navigation.review.ReviewManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal class HuaweiReviewManagerModule { + + @Provides + @Singleton + fun provideHuaweiReviewManager(): ReviewManager { + return DummyReviewManager() + } +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/review/DummyReviewManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/review/DummyReviewManager.kt new file mode 100644 index 0000000000..d418ec6905 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/review/DummyReviewManager.kt @@ -0,0 +1,5 @@ +package com.tangem.core.navigation.review + +class DummyReviewManager : ReviewManager { + override fun request(onDismissClick: () -> Unit) = Unit +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/review/ReviewManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/review/ReviewManager.kt new file mode 100644 index 0000000000..b7e577b4fa --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/review/ReviewManager.kt @@ -0,0 +1,12 @@ +package com.tangem.core.navigation.review + +/** + * Interface to manage in-app review requests. + */ +interface ReviewManager { + + /** + * Requests an in-app review flow. + */ + fun request(onDismissClick: () -> Unit) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt deleted file mode 100644 index 6f740b1c21..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/ScanCardToUnlockWalletClickHandler.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.domain - -import arrow.core.Either -import arrow.core.raise.either -import arrow.core.raise.ensure -import com.tangem.common.CompletionResult -import com.tangem.common.core.TangemSdkError -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.domain.card.ScanCardProcessor -import com.tangem.domain.wallets.builder.ColdUserWalletBuilder -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.usecase.SaveWalletUseCase -import javax.inject.Inject - -internal class ScanCardToUnlockWalletClickHandler @Inject constructor( - private val scanCardProcessor: ScanCardProcessor, - private val saveWalletUseCase: SaveWalletUseCase, - private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, -) { - - private var scanFailsCounter = 0 - - suspend operator fun invoke(walletId: UserWalletId): Either { - return either { - when (val result = scanCardProcessor.scan(analyticsSource = AnalyticsParam.ScreensSources.SignIn)) { - is CompletionResult.Failure -> { - if (result.error is TangemSdkError.UserCancelled) { - scanFailsCounter++ - ensure(scanFailsCounter < 2) { ScanCardToUnlockWalletError.ManyScanFails } - } - } - is CompletionResult.Success -> { - scanFailsCounter = 0 - - // If card's public key is null then user wallet will be null - val scannedWallet = coldUserWalletBuilderFactory.create(scanResponse = result.data).build() - - ensure(walletId == scannedWallet?.walletId) { - ScanCardToUnlockWalletError.WrongCardIsScanned - } - - if (scannedWallet != null) { - saveWalletUseCase(userWallet = scannedWallet, canOverride = true) - } - } - } - } - } -} - -internal sealed class ScanCardToUnlockWalletError { - - object WrongCardIsScanned : ScanCardToUnlockWalletError() - - object ManyScanFails : ScanCardToUnlockWalletError() -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt deleted file mode 100644 index a04cb17d49..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/ReviewManagerRequester.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.utils - -import android.app.Activity -import android.content.Context -import com.google.android.gms.tasks.Task -import com.google.android.play.core.review.ReviewInfo -import com.google.android.play.core.review.ReviewManager -import com.google.android.play.core.review.ReviewManagerFactory -import com.tangem.core.ui.utils.findActivity -import timber.log.Timber - -internal object ReviewManagerRequester { - - fun request(context: Context, onDismissClick: () -> Unit) { - val reviewManager = ReviewManagerFactory.create(context) - val requestTask = reviewManager.requestReviewFlow() - - requestTask - .addOnCompleteListener { - handleOnCompleteRequestTask( - reviewManager = reviewManager, - activity = context.findActivity(), - task = it, - onDismissClick = onDismissClick, - ) - } - .addOnFailureListener(Timber::e) - } - - private fun handleOnCompleteRequestTask( - reviewManager: ReviewManager, - activity: Activity, - task: Task, - onDismissClick: () -> Unit, - ) { - if (task.isSuccessful) { - val reviewFlow = reviewManager.launchReviewFlow(activity, task.result) - reviewFlow - .addOnCompleteListener { resultReviewTask -> - if (!resultReviewTask.isSuccessful) onDismissClick() - } - .addOnFailureListener(Timber::e) - } else { - Timber.e(task.exception) - } - } -} \ No newline at end of file From 931cb4d20bb4ff77bf0374f12796140ca71da0cf Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 18:37:15 +0500 Subject: [PATCH 010/106] Updated on 2026-08-14 --- .../intents/WalletContentClickIntents.kt | 17 ++- .../WalletCurrencyActionsClickIntents.kt | 97 ++++------------ .../intents/WalletWarningsClickIntents.kt | 109 +++++------------- .../wallet/state/model/WalletAlertState.kt | 106 ----------------- .../wallet/state/model/WalletAlertUM.kt | 96 +++++++++++++++ .../wallet/state/model/WalletEvent.kt | 4 - .../presentation/wallet/ui/WalletAlert.kt | 98 ---------------- .../wallet/ui/WalletEventEffect.kt | 8 -- .../presentation/wallet/ui/WalletScreen.kt | 16 +-- 9 files changed, 161 insertions(+), 390 deletions(-) delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertUM.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt 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 8ad405bb28..59c28e1d6d 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 @@ -7,6 +7,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -100,6 +101,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( private val accountDependencies: AccountDependencies, private val yieldSupplySetShouldShowMainPromoUseCase: YieldSupplySetShouldShowMainPromoUseCase, private val tokenListAnalyticsSender: TokenListAnalyticsSender, + private val uiMessageSender: UiMessageSender, ) : BaseWalletClickIntents(), WalletContentClickIntents { override fun onDetailsClick() { @@ -311,15 +313,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } override fun onConfirmDisposeExpressStatus() { - walletEventSender.send( - WalletEvent.ShowAlert( - WalletAlertState.ConfirmExpressStatusHide( - onConfirmClick = { - walletEventSender.onConsume() - onDisposeExpressStatus() - }, - onCancelClick = walletEventSender::onConsume, - ), + uiMessageSender.send( + WalletAlertUM.confirmExpressStatusHide( + onConfirmClick = { + walletEventSender.onConsume() + onDisposeExpressStatus() + }, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt index 78956da6ab..e6baa06b02 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletCurrencyActionsClickIntents.kt @@ -12,13 +12,14 @@ import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.WrappedList import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager +import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase @@ -56,7 +57,7 @@ import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState +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.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState @@ -150,6 +151,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val accountsFeatureToggles: AccountsFeatureToggles, private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, + private val uiMessageSender: UiMessageSender, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick( @@ -269,48 +271,19 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) modelScope.launch(dispatchers.main) { - walletEventSender.send( - event = WalletEvent.ShowAlert( - state = getHideTokeAlertConfig(userWalletId, cryptoCurrencyStatus), - ), - ) - } - } + val currency = cryptoCurrencyStatus.currency + val isCryptoCurrencyCoinCouldHide = currency is CryptoCurrency.Coin && + !isCryptoCurrencyCoinCouldHide(userWalletId = userWalletId, cryptoCurrencyCoin = currency) - private suspend fun getHideTokeAlertConfig( - userWalletId: UserWalletId, - cryptoCurrencyStatus: CryptoCurrencyStatus, - ): WalletAlertState.DefaultAlert { - val currency = cryptoCurrencyStatus.currency - val isCryptoCurrencyCoinCouldHide = currency is CryptoCurrency.Coin && - !isCryptoCurrencyCoinCouldHide(userWalletId = userWalletId, cryptoCurrencyCoin = currency) - return if (isCryptoCurrencyCoinCouldHide) { - WalletAlertState.DefaultAlert( - title = resourceReference( - id = R.string.token_details_unable_hide_alert_title, - formatArgs = WrappedList(listOf(cryptoCurrencyStatus.currency.name)), - ), - message = resourceReference( - id = R.string.token_details_unable_hide_alert_message, - formatArgs = WrappedList( - listOf( - cryptoCurrencyStatus.currency.name, - cryptoCurrencyStatus.currency.symbol, - cryptoCurrencyStatus.currency.network.name, - ), - ), - ), - onConfirmClick = null, - ) - } else { - WalletAlertState.DefaultAlert( - title = resourceReference( - id = R.string.token_details_hide_alert_title, - formatArgs = WrappedList(listOf(cryptoCurrencyStatus.currency.name)), - ), - message = resourceReference(R.string.token_details_hide_alert_message), - onConfirmClick = { onPerformHideToken(userWalletId, cryptoCurrencyStatus) }, - ) + if (isCryptoCurrencyCoinCouldHide) { + uiMessageSender.send(WalletAlertUM.unableHideToken(cryptoCurrency = cryptoCurrencyStatus.currency)) + } else { + uiMessageSender.send( + WalletAlertUM.hideTokenConfirm(cryptoCurrency = cryptoCurrencyStatus.currency) { + onPerformHideToken(userWalletId, cryptoCurrencyStatus) + }, + ) + } } } @@ -618,17 +591,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean { if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false - modelScope.launch(dispatchers.main) { - walletEventSender.send( - event = WalletEvent.ShowAlert( - state = WalletAlertState.DefaultAlert( - title = null, - message = unavailabilityReason.getUnavailabilityReasonText(), - onConfirmClick = null, - ), - ), - ) - } + uiMessageSender.send(DialogMessage(message = unavailabilityReason.getUnavailabilityReasonText())) return true } @@ -641,7 +604,10 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( modelScope.launch { statusFlow.foldStatus( onContent = { handleContent(route, eventCreator) }, - onError = { handleError(eventCreator = eventCreator) }, + onError = { + analyticsEventHandler.send(event = eventCreator(AnalyticsParam.Status.Error)) + uiMessageSender.send(WalletAlertUM.unavailableOperation()) + }, onLoading = { handleLoading(eventCreator) }, ) } @@ -667,21 +633,10 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( appRouter.push(route = route) } - private fun handleError( - alertState: WalletAlertState = WalletAlertState.UnavailableOperation, - eventCreator: (AnalyticsParam.Status) -> MainScreenAnalyticsEvent, - ) { - analyticsEventHandler.send(event = eventCreator(AnalyticsParam.Status.Error)) - - walletEventSender.send(event = WalletEvent.ShowAlert(state = alertState)) - } - private fun handleLoading(eventCreator: (AnalyticsParam.Status) -> MainScreenAnalyticsEvent) { analyticsEventHandler.send(event = eventCreator(AnalyticsParam.Status.Pending)) - walletEventSender.send( - event = WalletEvent.ShowAlert(state = WalletAlertState.ProvidersStillLoading), - ) + uiMessageSender.send(WalletAlertUM.providersStillLoading()) } private suspend fun getSwapRoute(targetRoute: AppRoute): AppRoute { @@ -743,12 +698,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private fun checkSwapCryptoAvailability(tokenCount: Int) { if (tokenCount < 2) { - handleError( - alertState = WalletAlertState.InsufficientTokensCountForSwapping, - eventCreator = MainScreenAnalyticsEvent::ButtonSwap, - ) - - return + analyticsEventHandler.send(event = MainScreenAnalyticsEvent.ButtonSwap(AnalyticsParam.Status.Error)) + uiMessageSender.send(WalletAlertUM.insufficientTokensCountForSwapping()) } } } \ 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 a439494235..cbadd05712 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 @@ -11,7 +11,7 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.navigation.review.ReviewManager import com.tangem.domain.card.SetCardWasScannedUseCase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.feedback.GetWalletMetaInfoUseCase @@ -38,14 +38,11 @@ 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.wallets.usecase.* -import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent 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.domain.ScanCardToUnlockWalletClickHandler -import com.tangem.feature.wallet.presentation.wallet.domain.ScanCardToUnlockWalletError import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState +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 @@ -70,8 +67,6 @@ internal interface WalletWarningsClickIntents { fun onOpenUnlockWalletsBottomSheetClick() - fun onUnlockVisaAccessClick() - fun onLikeAppClick() fun onDislikeAppClick() @@ -113,7 +108,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val neverToSuggestRateAppUseCase: NeverToSuggestRateAppUseCase, private val remindToRateAppLaterUseCase: RemindToRateAppLaterUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, - private val scanCardToUnlockWalletClickHandler: ScanCardToUnlockWalletClickHandler, private val nonBiometricUnlockWalletUseCase: NonBiometricUnlockWalletUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, @@ -133,6 +127,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase, private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase, private val uiMessageSender: UiMessageSender, + private val reviewManager: ReviewManager, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { @@ -212,38 +207,14 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } - override fun onUnlockVisaAccessClick() { - openScanCardDialog() - } - - private fun openScanCardDialog() { - modelScope.launch(dispatchers.main) { - scanCardToUnlockWalletClickHandler(walletId = stateHolder.getSelectedWalletId()) - .onLeft { error -> - when (error) { - ScanCardToUnlockWalletError.WrongCardIsScanned -> { - walletEventSender.send( - event = WalletEvent.ShowAlert(WalletAlertState.WrongCardIsScanned), - ) - } - ScanCardToUnlockWalletError.ManyScanFails -> router.openScanFailedDialog(::openScanCardDialog) - } - } - } - } - override fun onLikeAppClick() { analyticsEventHandler.send(MainScreen.NoticeRateAppButton(AnalyticsParam.RateApp.Liked)) - walletEventSender.send( - event = WalletEvent.RateApp( - onDismissClick = { - modelScope.launch(dispatchers.main) { - neverToSuggestRateAppUseCase() - } - }, - ), - ) + reviewManager.request { + modelScope.launch(dispatchers.main) { + neverToSuggestRateAppUseCase() + } + } } override fun onDislikeAppClick() { @@ -293,7 +264,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( action = PromotionBannerClicked.BannerAction.Closed(), ) }, - ) modelScope.launch(dispatchers.main) { shouldShowPromoWalletUseCase.neverToShow(promoId) @@ -374,21 +344,16 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonYes()) - walletEventSender.send( - event = WalletEvent.ShowAlert( - state = WalletAlertState.SimpleOkAlert( - message = resourceReference(R.string.warning_seedphrase_issue_answer_yes), - onOkClick = { - modelScope.launch { - seedPhraseNotificationUseCase.confirm(userWalletId = userWallet.walletId) + uiMessageSender.send( + WalletAlertUM.seedPhraseConfirm { + modelScope.launch { + seedPhraseNotificationUseCase.confirm(userWalletId = userWallet.walletId) - urlOpener.openUrl( - url = TangemBlogUrlBuilder.build(post = TangemBlogUrlBuilder.Post.SeedNotify), - ) - } - }, - ), - ), + urlOpener.openUrl( + url = TangemBlogUrlBuilder.build(post = TangemBlogUrlBuilder.Post.SeedNotify), + ) + } + }, ) } @@ -397,17 +362,12 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonNo()) - walletEventSender.send( - event = WalletEvent.ShowAlert( - state = WalletAlertState.SimpleOkAlert( - message = resourceReference(R.string.warning_seedphrase_issue_answer_no), - onOkClick = { - modelScope.launch { - seedPhraseNotificationUseCase.decline(userWalletId = userWallet.walletId) - } - }, - ), - ), + uiMessageSender.send( + WalletAlertUM.seedPhraseDismiss { + modelScope.launch { + seedPhraseNotificationUseCase.decline(userWalletId = userWallet.walletId) + } + }, ) } @@ -416,21 +376,16 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonUsed()) - walletEventSender.send( - event = WalletEvent.ShowAlert( - state = WalletAlertState.SimpleOkAlert( - message = resourceReference(R.string.warning_seedphrase_issue_answer_yes), - onOkClick = { - modelScope.launch { - seedPhraseNotificationUseCase.acceptSecond(userWalletId = userWallet.walletId) + uiMessageSender.send( + WalletAlertUM.seedPhraseConfirm { + modelScope.launch { + seedPhraseNotificationUseCase.acceptSecond(userWalletId = userWallet.walletId) - urlOpener.openUrl( - url = TangemBlogUrlBuilder.build(post = TangemBlogUrlBuilder.Post.SeedNotifySecond), - ) - } - }, - ), - ), + urlOpener.openUrl( + url = TangemBlogUrlBuilder.build(post = TangemBlogUrlBuilder.Post.SeedNotifySecond), + ) + } + }, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt deleted file mode 100644 index 3846271993..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.model - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.feature.wallet.impl.R - -@Immutable -internal sealed interface WalletAlertState { - - @Immutable - sealed class Basic : WalletAlertState { - abstract val title: TextReference? - abstract val message: TextReference - open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - open val isWarningConfirmButton: Boolean = false - abstract val onConfirmClick: (() -> Unit)? - open val cancelButtonText: TextReference? = null - open val onCancelClick: (() -> Unit)? = null - } - - @Immutable - sealed class TextInput : WalletAlertState { - abstract val title: TextReference - abstract val label: TextReference - open val text: String = "" - open val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - abstract val onConfirmClick: (String) -> Unit - abstract val errorTextProvider: (String) -> TextReference? - } - - data class SimpleOkAlert(val message: TextReference, val onOkClick: () -> Unit) : WalletAlertState - - data class DefaultAlert( - override val title: TextReference?, - override val message: TextReference, - override val onConfirmClick: (() -> Unit)?, - ) : Basic() - - data class VisaLimitsInfo( - val totalLimit: String, - val otherLimit: String, - ) : Basic() { - override val title: TextReference? = null - override val message: TextReference = stringReference( - value = "Limits are needed to control costs, improve security, manage risk. " + - "You can spend $totalLimit during the week for card payments in shops and " + - "$otherLimit for other transactions, e. g. subscriptions or debts.", - ) - override val onConfirmClick: (() -> Unit)? = null - } - - data object WrongCardIsScanned : Basic() { - override val title: TextReference = resourceReference(R.string.common_warning) - override val message: TextReference = resourceReference(R.string.error_wrong_wallet_tapped) - override val onConfirmClick: (() -> Unit)? = null - } - - data object RescanWallets : Basic() { - override val title: TextReference = resourceReference(R.string.common_attention) - override val message: TextReference = resourceReference(R.string.key_invalidated_warning_description) - override val onConfirmClick: (() -> Unit)? = null - } - - data object VisaBalancesInfo : Basic() { - override val title: TextReference? = null - override val message: TextReference = stringReference( - value = "Available balance is actual funds available, considering pending transactions, " + - "blocked amounts, and debit balance to prevent overdrafts.", - ) - override val onConfirmClick: (() -> Unit)? = null - } - - data object ProvidersStillLoading : Basic() { - override val title: TextReference = resourceReference(R.string.action_buttons_service_loading_alert_title) - override val message: TextReference = resourceReference(R.string.action_buttons_service_loading_alert_message) - override val onConfirmClick: (() -> Unit)? = null - } - - data object UnavailableOperation : Basic() { - override val title: TextReference = resourceReference(R.string.action_buttons_something_wrong_alert_title) - override val message: TextReference = resourceReference(R.string.action_buttons_something_wrong_alert_message) - override val onConfirmClick: (() -> Unit)? = null - } - - data object InsufficientTokensCountForSwapping : Basic() { - override val title: TextReference = - resourceReference(id = R.string.action_buttons_swap_no_tokens_added_alert_title) - - override val message: TextReference = - resourceReference(id = R.string.action_buttons_swap_no_tokens_added_alert_message) - - override val onConfirmClick: (() -> Unit)? = null - } - - data class ConfirmExpressStatusHide( - override val onConfirmClick: (() -> Unit), - override val onCancelClick: (() -> Unit), - ) : Basic() { - 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 confirmButtonText: TextReference = resourceReference(R.string.common_hide) - override val cancelButtonText: TextReference = resourceReference(R.string.common_cancel) - } -} \ No newline at end of file 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 new file mode 100644 index 0000000000..a0fe35468c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertUM.kt @@ -0,0 +1,96 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import com.tangem.core.ui.extensions.WrappedList +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction +import com.tangem.domain.models.currency.CryptoCurrency +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( + id = R.string.token_details_unable_hide_alert_title, + formatArgs = WrappedList(listOf(cryptoCurrency.name)), + ), + message = resourceReference( + id = R.string.token_details_unable_hide_alert_message, + formatArgs = WrappedList( + listOf( + cryptoCurrency.name, + cryptoCurrency.symbol, + cryptoCurrency.network.name, + ), + ), + ), + ) + } + + fun hideTokenConfirm(cryptoCurrency: CryptoCurrency, onClick: () -> Unit): DialogMessage { + return DialogMessage( + title = resourceReference( + id = R.string.token_details_hide_alert_title, + formatArgs = WrappedList(listOf(cryptoCurrency.name)), + ), + message = resourceReference(R.string.token_details_hide_alert_message), + firstActionBuilder = { + okAction(onClick) + }, + ) + } + + fun providersStillLoading(): DialogMessage { + return DialogMessage( + title = resourceReference(R.string.action_buttons_service_loading_alert_title), + message = resourceReference(R.string.action_buttons_service_loading_alert_message), + ) + } + + fun unavailableOperation(): DialogMessage { + return DialogMessage( + title = resourceReference(R.string.action_buttons_something_wrong_alert_title), + message = resourceReference(R.string.action_buttons_something_wrong_alert_message), + ) + } + + fun insufficientTokensCountForSwapping(): DialogMessage { + return DialogMessage( + title = resourceReference(R.string.action_buttons_swap_no_tokens_added_alert_title), + message = resourceReference(R.string.action_buttons_swap_no_tokens_added_alert_message), + ) + } + + fun confirmExpressStatusHide(onConfirmClick: () -> Unit): DialogMessage { + return 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 = onConfirmClick, + ) + }, + secondActionBuilder = { cancelAction() }, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt index b3f535d8a2..4e3b74f4cf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt @@ -13,12 +13,8 @@ internal sealed class WalletEvent { data class ShowError(val text: TextReference) : WalletEvent() - data class ShowAlert(val state: WalletAlertState) : WalletEvent() - data object CopyAddress : WalletEvent() - data class RateApp(val onDismissClick: () -> Unit) : WalletEvent() - data class DemonstrateWalletsScrollPreview(val direction: Direction) : WalletEvent() { enum class Direction { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt deleted file mode 100644 index 9ad537bad7..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletAlert.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui - -import androidx.compose.runtime.* -import androidx.compose.ui.text.input.TextFieldValue -import com.tangem.core.ui.components.AdditionalTextInputDialogUM -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.components.TextInputDialog -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState - -@Composable -internal fun WalletAlert(state: WalletAlertState, onDismiss: () -> Unit) { - when (state) { - is WalletAlertState.Basic -> BasicAlert(state, onDismiss) - is WalletAlertState.TextInput -> TextInputAlert(state, onDismiss) - is WalletAlertState.SimpleOkAlert -> { - BasicDialog( - message = state.message.resolveReference(), - confirmButton = DialogButtonUM( - onClick = { - state.onOkClick() - onDismiss() - }, - ), - onDismissDialog = onDismiss, - isDismissable = false, - ) - } - } -} - -@Composable -private fun BasicAlert(state: WalletAlertState.Basic, onDismiss: () -> Unit) { - val confirmButton: DialogButtonUM - val dismissButton: DialogButtonUM? - - val onActionClick = state.onConfirmClick - if (onActionClick != null) { - confirmButton = DialogButtonUM( - title = state.confirmButtonText.resolveReference(), - isWarning = state.isWarningConfirmButton, - onClick = { - onActionClick() - onDismiss() - }, - ) - dismissButton = DialogButtonUM( - title = stringResourceSafe(id = R.string.common_cancel), - onClick = onDismiss, - ) - } else { - confirmButton = DialogButtonUM( - title = state.confirmButtonText.resolveReference(), - isWarning = state.isWarningConfirmButton, - onClick = onDismiss, - ) - dismissButton = null - } - - BasicDialog( - message = state.message.resolveReference(), - confirmButton = confirmButton, - onDismissDialog = onDismiss, - title = state.title?.resolveReference(), - dismissButton = dismissButton, - ) -} - -@Composable -private fun TextInputAlert(state: WalletAlertState.TextInput, onDismiss: () -> Unit) { - var value by remember { mutableStateOf(TextFieldValue(text = state.text)) } - - TextInputDialog( - fieldValue = value, - confirmButton = DialogButtonUM( - title = state.confirmButtonText.resolveReference(), - isEnabled = value.text.isNotEmpty() && - value.text != state.text && - state.errorTextProvider(value.text) == null, - onClick = { - state.onConfirmClick(value.text) - onDismiss() - }, - ), - onDismissDialog = onDismiss, - onValueChange = { value = it }, - title = state.title.resolveReference(), - dismissButton = DialogButtonUM(title = stringResourceSafe(id = R.string.common_cancel), onClick = onDismiss), - textFieldParams = AdditionalTextInputDialogUM( - label = state.label.resolveReference(), - isError = state.errorTextProvider(value.text) != null, - caption = state.errorTextProvider(value.text)?.resolveReference(), - ), - ) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt index f1e5840afa..9f4df0e402 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt @@ -11,9 +11,7 @@ import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.utils.requestPermission import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent -import com.tangem.feature.wallet.presentation.wallet.ui.utils.ReviewManagerRequester import com.tangem.feature.wallet.presentation.wallet.ui.utils.animateScrollByIndex import com.tangem.feature.wallet.presentation.wallet.ui.utils.demonstrateScrolling import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION @@ -24,10 +22,8 @@ internal fun WalletEventEffect( snackbarHostState: SnackbarHostState, event: StateEvent, onAutoScrollSet: () -> Unit, - onAlertConfigSet: (WalletAlertState) -> Unit, ) { val coroutineScope = rememberCoroutineScope() - val context = LocalContext.current val resources = LocalContext.current.resources var showPermissionRequest by remember { mutableStateOf Unit, () -> Unit>?>(null) } @@ -57,10 +53,6 @@ internal fun WalletEventEffect( duration = SnackbarDuration.Short, ) } - is WalletEvent.ShowAlert -> onAlertConfigSet(value.state) - is WalletEvent.RateApp -> { - ReviewManagerRequester.request(context = context, onDismissClick = value.onDismissClick) - } is WalletEvent.DemonstrateWalletsScrollPreview -> { walletsListState.demonstrateScrolling(coroutineScope = coroutineScope, direction = value.direction) } 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 eacebf5eb3..a9f5d27170 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 @@ -100,13 +100,6 @@ internal fun WalletScreen( val snackbarHostState = remember(::SnackbarHostState) val isAutoScroll = remember { mutableStateOf(value = false) } - var alertConfig by remember { mutableStateOf(value = null) } - - val config = alertConfig - if (config != null) { - WalletAlert(state = config, onDismiss = { alertConfig = null }) - } - WalletContent( state = state, walletsListState = walletsListState, @@ -114,7 +107,6 @@ internal fun WalletScreen( isAutoScroll = isAutoScroll, onAutoScrollReset = { isAutoScroll.value = false }, bottomSheetContent = bottomSheetContent, - alertConfig = alertConfig, bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, onBottomSheetStateChange = onBottomSheetStateChange, ) @@ -124,7 +116,6 @@ internal fun WalletScreen( snackbarHostState = snackbarHostState, event = state.event, onAutoScrollSet = { isAutoScroll.value = true }, - onAlertConfigSet = { alertConfig = it }, ) } @@ -135,7 +126,6 @@ private fun WalletContent( walletsListState: LazyListState, snackbarHostState: SnackbarHostState, isAutoScroll: State, - alertConfig: WalletAlertState?, onAutoScrollReset: () -> Unit, bottomSheetHeaderHeightProvider: () -> Dp, onBottomSheetStateChange: (BottomSheetState) -> Unit, @@ -279,7 +269,6 @@ private fun WalletContent( selectedWallet = selectedWallet, snackbarHostState = snackbarHostState, bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, - alertConfig = alertConfig, onBottomSheetStateChange = onBottomSheetStateChange, bottomSheetContent = bottomSheetContent, content = scaffoldContent, @@ -294,7 +283,6 @@ private inline fun BaseScaffoldWithMarkets( listState: LazyListState, selectedWallet: WalletState, snackbarHostState: SnackbarHostState, - alertConfig: WalletAlertState?, bottomSheetHeaderHeightProvider: () -> Dp, noinline onBottomSheetStateChange: (BottomSheetState) -> Unit, crossinline bottomSheetContent: @Composable () -> Unit, @@ -341,7 +329,6 @@ private inline fun BaseScaffoldWithMarkets( BottomSheetStateEffects( bottomSheetState = bottomSheetState, - alertConfig = alertConfig, onBottomSheetStateChange = onBottomSheetStateChange, navigationBarVisible = isNavBarVisible, isSearchFieldFocused = isSearchFieldFocused, @@ -615,7 +602,6 @@ private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: ( @Composable private fun BottomSheetStateEffects( bottomSheetState: TangemSheetState, - alertConfig: WalletAlertState?, navigationBarVisible: MutableState, onBottomSheetStateChange: (BottomSheetState) -> Unit, isSearchFieldFocused: Boolean, @@ -634,7 +620,7 @@ private fun BottomSheetStateEffects( val isKeyboardVisible by rememberIsKeyboardVisible() LaunchedEffect(isKeyboardVisible) { - if (isKeyboardVisible && alertConfig == null && isSearchFieldFocused) { + if (isKeyboardVisible && isSearchFieldFocused) { bottomSheetState.expand() } } From 8d7a7926283c15b1dd4e43b3829f8313f84a8c23 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Feb 2026 11:54:41 +0200 Subject: [PATCH 011/106] Updated on 2026-08-14 --- .../configuration/utils/VersionNameProvider.kt | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/utils/VersionNameProvider.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/utils/VersionNameProvider.kt index 2cf83b5072..01e50a8273 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/utils/VersionNameProvider.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/utils/VersionNameProvider.kt @@ -60,17 +60,9 @@ internal class VersionNameProvider( val outputFile = project.rootProject.file("find-latest-release-branch.output") return try { - val execResult = project.providers.exec { + project.providers.exec { commandLine("sh", scriptPath.absolutePath, currentBranch) - isIgnoreExitValue = true - } - - val exitCode = execResult.result.get().exitValue - if (exitCode != 0) { - project.logger.warn("Script failed with exit code: $exitCode") - outputFile.delete() - return null - } + }.standardOutput.asText.get() if (!outputFile.exists()) { project.logger.warn("Script output file not found") From e3dea7cfb8106394e59427d7326ef8fd21a5fc18 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Feb 2026 17:37:36 +0400 Subject: [PATCH 012/106] Updated on 2026-08-14 --- .../send/v2/subcomponents/amount/model/SendAmountModel.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index b232d081f9..bc73a06d62 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -411,10 +411,8 @@ internal class SendAmountModel @Inject constructor( // Allowed only in multicurrency wallets val isMultiCurrency = userWallet?.isMultiCurrency == true - // Allowed only on networks without tx extras (e.i. memo and destination tag) - val isExtrasSupported = cryptoCurrencyStatus.currency.network.transactionExtrasType.isTxExtrasSupported() isSendWithSwapAvailable.update { - isAvailableForSwap && isMultiCurrency && !isExtrasSupported + isAvailableForSwap && isMultiCurrency } } } \ No newline at end of file From d86b3784b3f6769444a225a858e88d4a652462f4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Feb 2026 18:00:22 +0400 Subject: [PATCH 013/106] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 4 ++++ .../wallet/featuretoggles/WalletFeatureToggles.kt | 5 ++++- .../wallet/featuretoggles/DefaultWalletFeatureToggles.kt | 9 ++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) 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 1490dbb009..ac043f69bc 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 @@ -74,5 +74,9 @@ { "name": "EARN_BLOCK_ENABLED", "version": "undefined" + }, + { + "name": "WALLET_REORDER_FEATURE_ENABLED", + "version": "undefined" } ] diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt index 8621c8c0d1..c2b4b02ced 100644 --- a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt @@ -5,4 +5,7 @@ package com.tangem.features.wallet.featuretoggles * [REDACTED_AUTHOR] */ -interface WalletFeatureToggles \ No newline at end of file +interface WalletFeatureToggles { + + val isWalletReorderFeatureEnabled: Boolean +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt index f283fadd39..ea02a07cac 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt @@ -1,6 +1,13 @@ package com.tangem.feature.wallet.featuretoggles +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import javax.inject.Inject -internal class DefaultWalletFeatureToggles @Inject constructor() : WalletFeatureToggles \ No newline at end of file +internal class DefaultWalletFeatureToggles @Inject constructor( + private val featureToggles: FeatureTogglesManager, +) : WalletFeatureToggles { + + override val isWalletReorderFeatureEnabled: Boolean + get() = featureToggles.isFeatureEnabled("WALLET_REORDER_FEATURE_ENABLED") +} \ No newline at end of file From 52e7b5e58c489741e37c35778b4d21712362ad43 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Feb 2026 16:57:30 +0300 Subject: [PATCH 014/106] Updated on 2026-08-14 --- .../staking/single/DefaultSingleStakingBalanceProducer.kt | 6 +++--- .../single/DefaultSingleStakingBalanceProducerTest.kt | 2 +- .../features/account/fetcher/DefaultPortfolioFetcher.kt | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) 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 691773948a..bf66f62a4a 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 @@ -15,7 +15,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.map import timber.log.Timber /** @@ -44,7 +44,7 @@ internal class DefaultSingleStakingBalanceProducer @AssistedInject constructor( return multiStakingBalanceSupplier( params = MultiStakingBalanceProducer.Params(userWalletId = params.userWalletId), ) - .mapNotNull { balances -> + .map { balances -> val currentStakingId = params.stakingId val currentBalances = balances.filter { it.stakingId == currentStakingId } @@ -53,7 +53,7 @@ internal class DefaultSingleStakingBalanceProducer @AssistedInject constructor( currentStakingId = currentStakingId, currentBalances = currentBalances, analyticsExceptionHandler = analyticsExceptionHandler, - ) + ) ?: StakingBalance.Error(stakingId = currentStakingId) } .flowOn(dispatchers.default) } diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducerTest.kt index 774e23100c..f431e630e4 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducerTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleStakingBalanceProducerTest.kt @@ -200,7 +200,7 @@ internal class DefaultSingleStakingBalanceProducerTest { val actual = getEmittedValues(flow = producerFlow) // Assert - Truth.assertThat(actual).isEmpty() + Truth.assertThat(actual).containsExactly(StakingBalance.Error(stakingId = tonId)) verify(exactly = 1) { multiNetworkStatusSupplier(multiParams) } } diff --git a/features/account/impl/src/main/java/com/tangem/features/account/fetcher/DefaultPortfolioFetcher.kt b/features/account/impl/src/main/java/com/tangem/features/account/fetcher/DefaultPortfolioFetcher.kt index f8bf368a40..9fb0924c4a 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/fetcher/DefaultPortfolioFetcher.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/fetcher/DefaultPortfolioFetcher.kt @@ -78,6 +78,7 @@ internal class DefaultPortfolioFetcher @AssistedInject constructor( } private fun balancesForWallets(wallets: List): Flow> { + if (wallets.isEmpty()) return flowOf(emptyMap()) val balanceFlows = wallets.map { walletAccountsBalancesFlow(it) } return combine(balanceFlows) { pairs -> pairs.toMap() } } From aa1f4deb24bfd549624b42a6ab283fbc72a26552 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Feb 2026 16:51:25 +0500 Subject: [PATCH 015/106] Updated on 2026-08-14 --- .../common/ui/notifications/Notifications.kt | 66 +++++ .../tangem/core/ui/ds/badge/TangemBadge.kt | 7 +- .../tangem/core/ui/ds/badge/TangemBadgeUM.kt | 3 +- .../tangem/core/ui/ds/image/TangemIconUM.kt | 76 ++++++ .../core/ui/ds/message/TangemMessage.kt | 49 +++- .../core/ui/ds/message/TangemMessageEffect.kt | 63 ++++- .../core/ui/ds/message/TangemMessageUM.kt | 8 +- .../com/tangem/core/ui/ds/row/TangemRowUM.kt | 12 + .../core/ui/ds/row/header/TangemHeaderRow.kt | 230 ++++++++++++++++++ .../ui/ds/row/header/TangemHeaderRowUM.kt | 29 +++ .../core/ui/ds/row/token/TangemTokenRow.kt | 100 +++++++- .../core/ui/ds/row/token/TangemTokenRowUM.kt | 41 ++-- .../internal/TangemTokenRowPreviewData.kt | 98 ++++---- .../internal/TokenRowEndBottomContent.kt | 4 +- .../ds/row/token/internal/TokenRowSubtitle.kt | 4 +- .../ui/ds/row/token/internal/TokenRowTitle.kt | 8 +- .../core/ui/ds/topbar/TangemTopBarInner.kt | 9 +- .../core/ui/extensions/TextReference.kt | 7 + .../res/drawable/ic_attention_default_24.xml | 10 + .../res/drawable/ic_dollar_default_24.xml | 12 + .../res/drawable/ic_error_sync_default_24.xml | 12 + .../res/drawable/ic_exchange_default_24.xml | 12 + .../res/drawable/ic_filter_default_24.xml | 12 + .../main/res/drawable/ic_more_default_24.xml | 33 +++ .../main/res/drawable/ic_plus_default_24.xml | 13 + 25 files changed, 810 insertions(+), 108 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds/image/TangemIconUM.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowUM.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRowUM.kt create mode 100644 core/ui/src/main/res/drawable/ic_attention_default_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_dollar_default_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_error_sync_default_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_exchange_default_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_filter_default_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_more_default_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_plus_default_24.xml diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt index 68a3f77292..7f8ec96750 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt @@ -4,8 +4,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.ds.message.TangemMessage +import com.tangem.core.ui.ds.message.TangemMessageUM import com.tangem.core.ui.res.TangemTheme import kotlinx.collections.immutable.ImmutableList @@ -44,4 +48,66 @@ fun LazyListScope.notifications( ) }, ) +} + +/** + * Displays a list of notifications using TangemMessage composables. + * + * @param notifications List of NotificationConfig objects to be displayed. + * @param contentColor Color to be used for the content of the notifications. + * @param modifier Optional Modifier for the notifications. + * @param hasPaddingAbove Boolean indicating whether to add padding above the first notification. + */ +fun LazyListScope.notifications2( + notifications: ImmutableList, + contentColor: Color, + modifier: Modifier = Modifier, + hasPaddingAbove: Boolean = false, +) { + itemsIndexed( + items = notifications, + key = { index, item -> item.title?.hashCode()?.plus(index) ?: index }, + contentType = { _, item -> item::class.java }, + itemContent = { i, item -> + val topPadding = if (i == 0 && hasPaddingAbove) 0.dp else 12.dp + TangemMessage( + config = item, + contentColor = contentColor, + modifier = modifier + .padding(top = topPadding) + .animateItem(), + ) + }, + ) +} + +/** + * Displays a list of notifications using TangemMessage composables. + * + * @param notifications List of TangemMessageUM objects to be displayed. + * @param contentColor Color to be used for the content of the notifications. + * @param modifier Optional Modifier for the notifications. + * @param hasPaddingAbove Boolean indicating whether to add padding above the first notification. + */ +fun LazyListScope.notifications( + notifications: ImmutableList, + contentColor: Color, + modifier: Modifier = Modifier, + hasPaddingAbove: Boolean = false, +) { + itemsIndexed( + items = notifications, + key = { _, item -> item.id }, + contentType = { _, item -> item::class.java }, + itemContent = { i, item -> + val topPadding = if (i == 0 && hasPaddingAbove) TangemTheme.dimens2.x0 else TangemTheme.dimens2.x2 + TangemMessage( + messageUM = item, + contentColor = contentColor, + modifier = modifier + .padding(top = topPadding) + .animateItem(), + ) + }, + ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt index f41322e237..7f9f95b4d3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.ds.badge.TangemBadgeSize.* import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme @@ -48,6 +49,7 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) { color = badgeUM.color, type = badgeUM.type, iconPosition = badgeUM.iconPosition, + onClick = badgeUM.onClick, modifier = modifier, ) } @@ -64,6 +66,7 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) { * @param color [TangemBadgeColor] defining the color scheme of the badge. * @param type [TangemBadgeType] defining the style of the badge. * @param iconPosition [TangemBadgeIconPosition] defining icon position of the badge. + * @param onClick Lambda to be invoked when the badge is clicked (optional). * [REDACTED_AUTHOR] */ @@ -77,6 +80,7 @@ fun TangemBadge( color: TangemBadgeColor = TangemBadgeColor.Gray, type: TangemBadgeType = TangemBadgeType.Solid, iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start, + onClick: (() -> Unit)? = null, ) { val iconColor = getIconColor(type = type, color = color) Row( @@ -86,7 +90,8 @@ fun TangemBadge( .heightIn(min = size.toHeightDp()) .clip(shape.toShape(size)) .getBackgroundColor(type = type, color = color, shape = shape.toShape(size)) - .padding(size.toPaddingDp(position = iconPosition)), + .padding(size.toPaddingDp(position = iconPosition)) + .clickableSingle(enabled = onClick != null, onClick = { onClick?.invoke() }), ) { AnimatedVisibility( visible = iconRes != null && iconPosition == TangemBadgeIconPosition.Start, diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt index e28cbdd6ba..5f666898bb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt @@ -8,13 +8,13 @@ import com.tangem.core.ui.extensions.TextReference * UI model for [TangemBadge] component * * @param text TextReference for the badge label. - * @param modifier Modifier to be applied to the badge. * @param iconRes Drawable resource ID for the icon to be displayed in the badge. * @param size [TangemBadgeSize] defining the size of the badge. * @param shape [TangemBadgeShape] defining the shape of the badge. * @param color [TangemBadgeColor] defining the color scheme of the badge. * @param type [TangemBadgeType] defining the style of the badge. * @param iconPosition [TangemBadgeIconPosition] defining icon position of the badge. + * @param onClick Lambda to be invoked when the badge is clicked (optional). */ class TangemBadgeUM( val text: TextReference, @@ -24,4 +24,5 @@ class TangemBadgeUM( val color: TangemBadgeColor = TangemBadgeColor.Gray, val type: TangemBadgeType = TangemBadgeType.Solid, val iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start, + val onClick: (() -> Unit)? = null, ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/image/TangemIconUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/image/TangemIconUM.kt new file mode 100644 index 0000000000..ff7be95be2 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/image/TangemIconUM.kt @@ -0,0 +1,76 @@ +package com.tangem.core.ui.ds.image + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.icons.identicon.IdentIcon +import com.tangem.core.ui.extensions.ColorReference2 +import com.tangem.core.ui.res.TangemTheme + +/** + * Model representing different types of icons that can be displayed in the UI. + */ +@Immutable +sealed interface TangemIconUM { + + /** Icon representing a currency. */ + data class Currency( + val currencyIconState: CurrencyIconState, + ) : TangemIconUM + + /** Icon represented by a drawable resource. */ + data class Icon( + @DrawableRes val iconRes: Int, + val tintReference: ColorReference2 = ColorReference2 { TangemTheme.colors2.graphic.neutral.primary }, + ) : TangemIconUM + + /** Image represented by a drawable resource. */ + data class Image( + @DrawableRes val imageRes: Int, + ) : TangemIconUM + + /** Identicon represented by a text string (e.g., an address). */ + data class Ident( + val text: String, + ) : TangemIconUM +} + +/** + * Composable function to display an icon based on the provided [TangemIconUM] type. + * + * @param tangemIconUM The [TangemIconUM] instance representing the icon to be displayed. + * @param modifier The [Modifier] to be applied to the icon. + */ +@Composable +fun TangemIcon(tangemIconUM: TangemIconUM, modifier: Modifier = Modifier) { + when (tangemIconUM) { + is TangemIconUM.Currency -> { + CurrencyIcon( + state = tangemIconUM.currencyIconState, + modifier = modifier, + ) + } + is TangemIconUM.Icon -> Icon( + imageVector = ImageVector.vectorResource(tangemIconUM.iconRes), + contentDescription = null, + modifier = modifier, + tint = tangemIconUM.tintReference(), + ) + is TangemIconUM.Image -> Image( + imageVector = ImageVector.vectorResource(tangemIconUM.imageRes), + contentDescription = null, + modifier = modifier, + ) + is TangemIconUM.Ident -> IdentIcon( + address = tangemIconUM.text, + modifier = modifier, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt index ff63624792..3591920330 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt @@ -11,9 +11,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource +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.PreviewParameterProvider @@ -24,6 +26,7 @@ import com.tangem.core.ui.components.flicker import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.ds.button.* +import com.tangem.core.ui.ds.image.TangemIcon import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @@ -35,21 +38,31 @@ import kotlinx.collections.immutable.persistentListOf * * @param messageUM Data model containing message properties. * @param modifier Modifier to be applied to the message component. - * @param content Optional composable content to be displayed alongside the title and subtitle. */ @Composable fun TangemMessage( messageUM: TangemMessageUM, modifier: Modifier = Modifier, - content: @Composable (RowScope.() -> Unit)? = null, + contentColor: Color = TangemTheme.colors2.surface.level3, ) { TangemMessage( - modifier = modifier, + modifier = modifier + .conditional(messageUM.onClick != null) { + clickableSingle(onClick = requireNotNull(messageUM.onClick)) + }, title = messageUM.title, subtitle = messageUM.subtitle, messageEffect = messageUM.messageEffect, isCentered = messageUM.isCentered, - content = content, + content = { + if (messageUM.iconUM != null) { + TangemIcon( + tangemIconUM = messageUM.iconUM, + modifier = Modifier.size(TangemTheme.dimens2.x8), + ) + } + }, + contentColor = contentColor, onCloseClick = messageUM.onCloseClick, buttons = { messageUM.buttonsUM.fastForEach { buttonUM -> @@ -73,7 +86,11 @@ fun TangemMessage( * @see com.tangem.core.ui.components.notifications.Notification for legacy component. */ @Composable -fun TangemMessage(config: NotificationConfig, modifier: Modifier = Modifier) { +fun TangemMessage( + config: NotificationConfig, + modifier: Modifier = Modifier, + contentColor: Color = TangemTheme.colors2.surface.level3, +) { val buttonState = config.buttonsState TangemMessage( title = config.title, @@ -101,6 +118,7 @@ fun TangemMessage(config: NotificationConfig, modifier: Modifier = Modifier) { ) } }, + contentColor = contentColor, buttons = if (buttonState != null) { { TangemMessageLegacyButtons(buttonState = buttonState) @@ -129,10 +147,11 @@ fun TangemMessage( title: TextReference? = null, subtitle: TextReference? = null, messageEffect: TangemMessageEffect = TangemMessageEffect.None, - content: (@Composable RowScope.() -> Unit)? = null, - buttons: (@Composable RowScope.() -> Unit)? = null, onCloseClick: (() -> Unit)? = null, isCentered: Boolean = false, + contentColor: Color = TangemTheme.colors2.surface.level3, + content: (@Composable RowScope.() -> Unit)? = null, + buttons: (@Composable RowScope.() -> Unit)? = null, ) { val alignment = if (isCentered) { Alignment.CenterHorizontally @@ -146,6 +165,7 @@ fun TangemMessage( .messageEffectBackground( messageEffect = messageEffect, radius = TangemTheme.dimens2.x6, + contentColor = contentColor, ), ) Column( @@ -160,8 +180,9 @@ fun TangemMessage( subtitle = subtitle, alignment = alignment, content = content, + isCentered = isCentered, ) - if (buttons != null && !isCentered) { + if (buttons != null) { Row( horizontalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier @@ -191,8 +212,14 @@ private fun TangemMessageContent( title: TextReference? = null, subtitle: TextReference? = null, alignment: Alignment.Horizontal = Alignment.Start, + isCentered: Boolean = false, content: (@Composable RowScope.() -> Unit)? = null, ) { + val textAlign = if (isCentered) { + TextAlign.Center + } else { + TextAlign.Start + } Row( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), modifier = Modifier.padding(TangemTheme.dimens2.x1), @@ -208,10 +235,12 @@ private fun TangemMessageContent( style = TangemTheme.typography2.bodySemibold16, color = TangemTheme.colors2.text.neutral.primary, maxLines = 1, + textAlign = textAlign, ) } if (subtitle != null) { Text( + textAlign = textAlign, text = subtitle.resolveAnnotatedReference(), style = TangemTheme.typography2.captionSemibold12, color = TangemTheme.colors2.text.neutral.secondary, @@ -310,12 +339,14 @@ private class TangemMessagePreviewProvider : PreviewParameterProvider get() = sequenceOf( TangemMessageUM( + id = "1", title = stringReference("Title text"), subtitle = stringReference("Subtext"), messageEffect = TangemMessageEffect.None, isCentered = true, ), TangemMessageUM( + id = "2", title = stringReference("Title text"), subtitle = stringReference("Subtext"), messageEffect = TangemMessageEffect.Magic, @@ -335,6 +366,7 @@ private class TangemMessagePreviewProvider : PreviewParameterProvider = persistentListOf(), + val onClick: (() -> Unit)? = null, val onCloseClick: (() -> Unit)? = null, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowUM.kt new file mode 100644 index 0000000000..d605343ac6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowUM.kt @@ -0,0 +1,12 @@ +package com.tangem.core.ui.ds.row + +import androidx.compose.runtime.Immutable + +/** + * Base interface for all row UI models in the Tangem application. Each row UI model must implement this interface + */ +@Immutable +interface TangemRowUM { + + val id: String +} \ No newline at end of file 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 new file mode 100644 index 0000000000..f5af27f6c1 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt @@ -0,0 +1,230 @@ +package com.tangem.core.ui.ds.row.header + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.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.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.ds.image.TangemIcon +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.clickableSingle +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.test.TokenElementsTestTags + +/** + * UI model for header row component + * + * @param headerRowUM UI model for the header row + * @param modifier Modifier for the composable + */ +@Composable +fun TangemHeaderRow(headerRowUM: TangemHeaderRowUM, modifier: Modifier = Modifier) { + TangemHeaderRow( + headTangemIconUM = headerRowUM.startIconUM, + footerTangemIconRes = headerRowUM.endIconRes, + title = headerRowUM.title, + subtitle = headerRowUM.subtitle, + modifier = modifier, + ) +} + +/** + * Composable function that represents a header row with customizable title and head content. + * + * @param modifier Modifier for the composable + * @param subtitle Optional subtitle as a TextReference + * @param onItemClick Optional click callback for the row + * @param footerTangemIconRes Optional drawable resource ID for the footer icon + * @param titleContent Composable lambda for the title content + * @param headContent Composable lambda for the head content + */ +@Composable +fun TangemHeaderRow( + modifier: Modifier = Modifier, + subtitle: TextReference? = null, + onItemClick: (() -> Unit)? = null, + @DrawableRes footerTangemIconRes: Int? = null, + titleContent: @Composable (Modifier) -> Unit, + headContent: @Composable (Modifier) -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .clickableSingle(enabled = onItemClick != null, onClick = { onItemClick?.invoke() }) + .padding( + top = TangemTheme.dimens2.x4, + bottom = TangemTheme.dimens2.x3, + start = TangemTheme.dimens2.x4, + end = TangemTheme.dimens2.x4, + ), + ) { + headContent( + Modifier + .padding(end = TangemTheme.dimens2.x2) + .size(TangemTheme.dimens2.x4), + ) + titleContent(Modifier) + AnimatedVisibility( + visible = subtitle != null, + ) { + val wrappedSubtitle = remember(this) { requireNotNull(subtitle) } + Text( + text = wrappedSubtitle.resolveAnnotatedReference(), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + modifier = Modifier + .testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT) + .padding(start = TangemTheme.dimens2.x1), + ) + } + SpacerWMax() + AnimatedVisibility( + visible = footerTangemIconRes != null, + ) { + val wrappedIconUM = remember(this) { requireNotNull(footerTangemIconRes) } + Icon( + imageVector = ImageVector.vectorResource(id = wrappedIconUM), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.secondary, + modifier = Modifier.size(TangemTheme.dimens2.x4), + ) + } + } +} + +/** + * Composable function that represents a header row with title, optional subtitle, and optional icons. + * + * @param title Title as a TextReference + * @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 + */ +@Composable +fun TangemHeaderRow( + title: TextReference, + modifier: Modifier = Modifier, + subtitle: TextReference? = null, + headTangemIconUM: TangemIconUM? = null, + @DrawableRes footerTangemIconRes: Int? = null, + isEnabled: Boolean = false, + onItemClick: (() -> Unit)? = null, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .clickableSingle(enabled = isEnabled && onItemClick != null, onClick = { onItemClick?.invoke() }) + .padding( + top = TangemTheme.dimens2.x4, + bottom = TangemTheme.dimens2.x3, + start = TangemTheme.dimens2.x4, + end = TangemTheme.dimens2.x4, + ), + ) { + AnimatedVisibility( + visible = headTangemIconUM != null, + ) { + val wrappedIconUM = remember(this) { requireNotNull(headTangemIconUM) } + TangemIcon( + tangemIconUM = wrappedIconUM, + modifier = Modifier + .padding(end = TangemTheme.dimens2.x2) + .size(TangemTheme.dimens2.x4), + ) + } + Text( + text = title.resolveAnnotatedReference(), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + modifier = Modifier.testTag(tag = TokenElementsTestTags.TOKEN_TITLE), + ) + AnimatedVisibility( + visible = subtitle != null, + ) { + val wrappedSubtitle = remember(this) { requireNotNull(subtitle) } + Text( + text = wrappedSubtitle.resolveAnnotatedReference(), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + modifier = Modifier + .testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT) + .padding(start = TangemTheme.dimens2.x1), + ) + } + SpacerWMax() + AnimatedVisibility( + visible = footerTangemIconRes != null, + ) { + val wrappedIconUM = remember(this) { requireNotNull(footerTangemIconRes) } + Icon( + imageVector = ImageVector.vectorResource(id = wrappedIconUM), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.secondary, + modifier = Modifier.size(TangemTheme.dimens2.x4), + ) + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TangemHeaderRow_Preview(@PreviewParameter(PreviewProvider::class) params: TangemHeaderRowUM) { + TangemThemePreviewRedesign { + TangemHeaderRow( + headerRowUM = params, + modifier = Modifier.background(TangemTheme.colors2.surface.level3), + ) + } +} + +private class PreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + TangemHeaderRowUM( + id = "1", + startIconUM = TangemIconUM.Currency( + currencyIconState = CurrencyIconState.Locked, + ), + endIconRes = R.drawable.ic_minimize_24, + title = stringReference("Account"), + subtitle = stringReference("\$ 42,900.17"), + ), + TangemHeaderRowUM( + id = "2", + title = stringReference("Account"), + ), + ) +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRowUM.kt new file mode 100644 index 0000000000..fd69abb660 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRowUM.kt @@ -0,0 +1,29 @@ +package com.tangem.core.ui.ds.row.header + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.row.TangemRowUM +import com.tangem.core.ui.extensions.TextReference + +/** + * UI model for header row component + * + * @param id Unique id + * @param title Title text reference + * @param subtitle Subtitle text reference (optional) + * @param startIconUM Icon UI model (optional) + * @param endIconRes Icon UI model (optional) + * @param isEnabled Flag indicating if click is enabled + * @param onItemClick Callback for item click (optional) + */ +@Immutable +data class TangemHeaderRowUM( + override val id: String, + val title: TextReference, + val subtitle: TextReference? = null, + val startIconUM: TangemIconUM? = null, + @DrawableRes val endIconRes: Int? = null, + val isEnabled: Boolean = false, + val onItemClick: (() -> Unit)? = null, +) : TangemRowUM \ No newline at end of file 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 8f9c251ea1..a0b50fe199 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 @@ -16,7 +16,7 @@ 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 com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.ds.image.TangemIcon import com.tangem.core.ui.ds.row.TangemRowContainer import com.tangem.core.ui.ds.row.TangemRowLayoutId import com.tangem.core.ui.ds.row.token.internal.* @@ -44,19 +44,19 @@ fun TangemTokenRow( ) { TangemRowContainer( content = { - CurrencyIcon( - state = tokenRowUM.iconState, + TangemIcon( + tangemIconUM = tokenRowUM.headIconUM, modifier = Modifier .layoutId(layoutId = TangemRowLayoutId.HEAD) .padding(end = TangemTheme.dimens2.x2) - .testTag(TokenElementsTestTags.TOKEN_ICON), + .testTag(tag = TokenElementsTestTags.TOKEN_ICON), ) TokenRowPromoBanner( promoBannerUM = tokenRowUM.promoBannerUM, modifier = Modifier .layoutId(layoutId = TangemRowLayoutId.EXTRA_TOP) - .testTag(TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER) + .testTag(tag = TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER) .padding(horizontal = TangemTheme.dimens2.x3) .fillMaxWidth(), ) @@ -98,7 +98,89 @@ fun TangemTokenRow( reorderableTokenListState = reorderableTokenListState, modifier = Modifier .layoutId(layoutId = TangemRowLayoutId.TAIL) - .testTag(TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK), + .testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK), + ) + }, + modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM), + ) +} + +/** + * Composable function that represents a Tangem token row in a list. + * + * [Token Row](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8207-17583&t=k8dyaykorsNocGVq-4) + * + * @param tokenRowUM The user model containing the data for the token row. + * @param headComponent The composable function representing the head component. + * @param titleComponent The composable function representing the title component. + * @param isBalanceHidden A boolean indicating whether the balance should be hidden. + * @param reorderableTokenListState The state of the reorderable lazy list, if applicable. + * @param modifier The modifier to be applied to the row. + */ +@Composable +fun TangemTokenRow( + tokenRowUM: TangemTokenRowUM, + isBalanceHidden: Boolean, + reorderableTokenListState: ReorderableLazyListState?, + modifier: Modifier = Modifier, + headComponent: @Composable (Modifier) -> Unit, + titleComponent: @Composable (Modifier) -> Unit, +) { + TangemRowContainer( + content = { + headComponent( + Modifier + .layoutId(layoutId = TangemRowLayoutId.HEAD) + .padding(end = TangemTheme.dimens2.x2) + .testTag(tag = TokenElementsTestTags.TOKEN_ICON), + ) + + TokenRowPromoBanner( + promoBannerUM = tokenRowUM.promoBannerUM, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.EXTRA_TOP) + .testTag(tag = TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER) + .padding(horizontal = TangemTheme.dimens2.x3) + .fillMaxWidth(), + ) + + titleComponent( + Modifier + .layoutId(layoutId = TangemRowLayoutId.START_TOP) + .padding(end = TangemTheme.dimens2.x2) + .testTag(tag = TokenElementsTestTags.TOKEN_TITLE), + ) + + TokenRowSubtitle( + subtitleUM = tokenRowUM.subtitleUM, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.START_BOTTOM) + .padding(end = TangemTheme.dimens2.x2) + .testTag(tag = TokenElementsTestTags.TOKEN_PRICE), + ) + + TokenRowEndTopContent( + endContentUM = tokenRowUM.topEndContentUM, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.END_TOP) + .testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT), + ) + + TokenRowEndBottomContent( + endContentUM = tokenRowUM.bottomEndContentUM, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.END_BOTTOM) + .testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT), + ) + + TokenRowTail( + tailUM = tokenRowUM.tailUM, + reorderableTokenListState = reorderableTokenListState, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.TAIL) + .testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK), ) }, modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM), @@ -114,7 +196,7 @@ private fun Modifier.tokenClickable(tokenRowUM: TangemTokenRowUM): Modifier = co val onHapticLongClick = if (onLongClick != null) { { hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - onLongClick(tokenRowUM) + onLongClick() } } else { null @@ -123,9 +205,9 @@ private fun Modifier.tokenClickable(tokenRowUM: TangemTokenRowUM): Modifier = co when { onClick == null && onLongClick == null -> this onClick == null && onLongClick != null -> combinedClickable(onClick = {}, onLongClick = onHapticLongClick) - onClick != null && onLongClick == null -> combinedClickable(onClick = { onClick(tokenRowUM) }) + onClick != null && onLongClick == null -> combinedClickable(onClick = onClick) onClick != null && onLongClick != null -> { - combinedClickable(onClick = { onClick(tokenRowUM) }, onLongClick = onHapticLongClick) + combinedClickable(onClick = onClick, onLongClick = onHapticLongClick) } else -> this } 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 8ef54a567b..98027ff5dd 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 @@ -4,20 +4,20 @@ import androidx.compose.runtime.Immutable 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 -import com.tangem.core.ui.extensions.ColorReference2 +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.row.TangemRowUM import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.res.TangemTheme import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @Immutable -sealed class TangemTokenRowUM { +sealed class TangemTokenRowUM : TangemRowUM { /** Unique id */ - abstract val id: String + abstract override val id: String /** Token icon state */ - abstract val iconState: CurrencyIconState + abstract val headIconUM: TangemIconUM.Currency /** Token title UM (in one row with [topEndContentUM]) */ abstract val titleUM: TitleUM @@ -38,25 +38,25 @@ sealed class TangemTokenRowUM { abstract val promoBannerUM: PromoBannerUM /** Callback which will be called when an item is clicked */ - abstract val onItemClick: ((TangemTokenRowUM) -> Unit)? + abstract val onItemClick: (() -> Unit)? /** Callback which will be called when an item is long clicked */ - abstract val onItemLongClick: ((TangemTokenRowUM) -> Unit)? + abstract val onItemLongClick: (() -> Unit)? /** * Content state of [TangemTokenRowUM] */ data class Content( override val id: String, - override val iconState: CurrencyIconState, + override val headIconUM: TangemIconUM.Currency, override val titleUM: TitleUM, override val subtitleUM: SubtitleUM, override val topEndContentUM: EndContentUM, override val bottomEndContentUM: EndContentUM, override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty, override val tailUM: TailUM = TailUM.Empty, - override val onItemClick: ((TangemTokenRowUM) -> Unit)?, - override val onItemLongClick: ((TangemTokenRowUM) -> Unit)?, + override val onItemClick: (() -> Unit)?, + override val onItemLongClick: (() -> Unit)?, ) : TangemTokenRowUM() /** @@ -64,7 +64,7 @@ sealed class TangemTokenRowUM { */ data class Loading( override val id: String, - override val iconState: CurrencyIconState, + override val headIconUM: TangemIconUM.Currency = TangemIconUM.Currency(CurrencyIconState.Loading), override val titleUM: TitleUM = TitleUM.Loading, override val subtitleUM: SubtitleUM = SubtitleUM.Loading, ) : TangemTokenRowUM() { @@ -72,8 +72,8 @@ sealed class TangemTokenRowUM { override val bottomEndContentUM: EndContentUM = EndContentUM.Loading override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty override val tailUM: TailUM = TailUM.Empty - override val onItemClick: ((TangemTokenRowUM) -> Unit)? = null - override val onItemLongClick: ((TangemTokenRowUM) -> Unit)? = null + override val onItemClick: (() -> Unit)? = null + override val onItemLongClick: (() -> Unit)? = null } /** @@ -81,12 +81,12 @@ sealed class TangemTokenRowUM { */ data class Actionable( override val id: String, - override val iconState: CurrencyIconState, + override val headIconUM: TangemIconUM.Currency, override val titleUM: TitleUM, override val subtitleUM: SubtitleUM, override val tailUM: TailUM, - override val onItemClick: ((TangemTokenRowUM) -> Unit)?, - override val onItemLongClick: ((TangemTokenRowUM) -> Unit)?, + override val onItemClick: (() -> Unit)?, + override val onItemLongClick: (() -> Unit)?, override val topEndContentUM: EndContentUM = EndContentUM.Empty, override val bottomEndContentUM: EndContentUM = EndContentUM.Empty, ) : TangemTokenRowUM() { @@ -116,7 +116,7 @@ sealed class TangemTokenRowUM { val text: TextReference, val isAvailable: Boolean = true, val isFlickering: Boolean = false, - val icons: ImmutableList = persistentListOf(), + val icons: ImmutableList = persistentListOf(), val priceChangeUM: PriceChangeState = PriceChangeState.Unknown, val badge: TangemBadgeUM? = null, ) : SubtitleUM() @@ -133,7 +133,7 @@ sealed class TangemTokenRowUM { val text: TextReference, val isAvailable: Boolean = true, val isFlickering: Boolean = false, - val icons: ImmutableList = persistentListOf(), + val icons: ImmutableList = persistentListOf(), val priceChangeUM: PriceChangeState = PriceChangeState.Unknown, ) : EndContentUM() @@ -164,9 +164,4 @@ sealed class TangemTokenRowUM { data object Empty : TailUM() } - - data class IconUM( - val iconRes: Int, - val tintReference: ColorReference2 = ColorReference2 { TangemTheme.colors2.graphic.neutral.primary }, - ) } \ 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 aee2125611..ce68a8771d 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 @@ -6,6 +6,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.stringReference @@ -63,49 +64,60 @@ internal object TangemTokenRowPreviewData { text = stringReference("Title"), ) - private val accountResIcon: CurrencyIconState.CryptoPortfolio.Icon - get() = CurrencyIconState.CryptoPortfolio.Icon( - resId = R.drawable.ic_rounded_star_24, - color = Color.Blue, - isGrayscale = false, - ) - private val accountLetterIcon: CurrencyIconState.CryptoPortfolio.Letter - get() = CurrencyIconState.CryptoPortfolio.Letter( - char = stringReference("A"), - color = Color.Blue, - isGrayscale = false, + private val accountResIcon: TangemIconUM.Currency + get() = TangemIconUM.Currency( + CurrencyIconState.CryptoPortfolio.Icon( + resId = R.drawable.ic_rounded_star_24, + color = Color.Blue, + isGrayscale = false, + ), ) - private val coinIconState - get() = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = R.drawable.img_polygon_22, - isGrayscale = false, - shouldShowCustomBadge = false, + private val accountLetterIcon: TangemIconUM.Currency + get() = TangemIconUM.Currency( + CurrencyIconState.CryptoPortfolio.Letter( + char = stringReference("A"), + color = Color.Blue, + isGrayscale = false, + ), ) - private val tokenIconState - get() = CurrencyIconState.TokenIcon( - url = null, - topBadgeIconResId = R.drawable.img_polygon_22, - fallbackTint = TangemColorPalette.Black, - fallbackBackground = TangemColorPalette.Meadow, - isGrayscale = false, - shouldShowCustomBadge = false, + private val coinIconState: TangemIconUM.Currency + get() = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_polygon_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), ) - private val customTokenIconState - get() = CurrencyIconState.CustomTokenIcon( - tint = TangemColorPalette.Black, - background = TangemColorPalette.Meadow, - topBadgeIconResId = R.drawable.img_polygon_22, - isGrayscale = false, + private val tokenIconState: TangemIconUM.Currency + get() = TangemIconUM.Currency( + CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_polygon_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ) + + private val customTokenIconState: TangemIconUM.Currency + get() = TangemIconUM.Currency( + CurrencyIconState.CustomTokenIcon( + tint = TangemColorPalette.Black, + background = TangemColorPalette.Meadow, + topBadgeIconResId = R.drawable.img_polygon_22, + isGrayscale = false, + ), ) val defaultState: TangemTokenRowUM.Content get() = TangemTokenRowUM.Content( id = UUID.randomUUID().toString(), - iconState = coinIconState, + headIconUM = coinIconState, titleUM = titleUM, subtitleUM = subtitleUM, topEndContentUM = topEndContentUM, @@ -131,9 +143,9 @@ internal object TangemTokenRowPreviewData { }), ), icons = persistentListOf( - TangemTokenRowUM.IconUM(R.drawable.ic_staking_mini_10), - TangemTokenRowUM.IconUM(R.drawable.ic_attention_12), - TangemTokenRowUM.IconUM(R.drawable.ic_error_sync_24), + TangemIconUM.Icon(R.drawable.ic_staking_mini_10), + TangemIconUM.Icon(R.drawable.ic_attention_12), + TangemIconUM.Icon(R.drawable.ic_error_sync_24), ), ), bottomEndContentUM = bottomEndContentUM, @@ -146,7 +158,7 @@ internal object TangemTokenRowPreviewData { val tokenState: TangemTokenRowUM.Content get() = TangemTokenRowUM.Content( id = UUID.randomUUID().toString(), - iconState = tokenIconState, + headIconUM = tokenIconState, titleUM = titleUM, subtitleUM = subtitleUM, topEndContentUM = topEndContentUM, @@ -160,7 +172,7 @@ internal object TangemTokenRowPreviewData { val customTokenState: TangemTokenRowUM.Content get() = TangemTokenRowUM.Content( id = UUID.randomUUID().toString(), - iconState = customTokenIconState, + headIconUM = customTokenIconState, titleUM = titleUM, subtitleUM = subtitleUM, topEndContentUM = topEndContentUM, @@ -174,7 +186,7 @@ internal object TangemTokenRowPreviewData { val draggableState: TangemTokenRowUM.Actionable get() = TangemTokenRowUM.Actionable( id = UUID.randomUUID().toString(), - iconState = coinIconState, + headIconUM = coinIconState, titleUM = titleUM, subtitleUM = subtitleUM, tailUM = TangemTokenRowUM.TailUM.Draggable, @@ -185,7 +197,7 @@ internal object TangemTokenRowPreviewData { val draggableStateV2: TangemTokenRowUM.Actionable get() = TangemTokenRowUM.Actionable( id = UUID.randomUUID().toString(), - iconState = coinIconState, + headIconUM = coinIconState, titleUM = titleUM, subtitleUM = subtitleUM, topEndContentUM = topEndContentUM, @@ -198,12 +210,12 @@ internal object TangemTokenRowPreviewData { val loadingState: TangemTokenRowUM.Loading get() = TangemTokenRowUM.Loading( id = UUID.randomUUID().toString(), - iconState = coinIconState, + headIconUM = coinIconState, titleUM = TangemTokenRowUM.TitleUM.Loading, subtitleUM = TangemTokenRowUM.SubtitleUM.Loading, ) - val unreachableState: TangemTokenRowUM.Content + val unreachableState: TangemTokenRowUM get() = defaultState.copy( topEndContentUM = TangemTokenRowUM.EndContentUM.Content( text = stringReference(StringsSigns.DASH_SIGN), @@ -219,7 +231,7 @@ internal object TangemTokenRowPreviewData { val accountState: TangemTokenRowUM.Content get() = TangemTokenRowUM.Content( id = UUID.randomUUID().toString(), - iconState = accountResIcon, + headIconUM = accountResIcon, titleUM = TangemTokenRowUM.TitleUM.Content( text = stringReference(value = "Portfolio"), ), @@ -239,7 +251,7 @@ internal object TangemTokenRowPreviewData { val accountLetterState: TangemTokenRowUM.Content get() = accountState.copy( - iconState = accountLetterIcon, + headIconUM = accountLetterIcon, ) val accountEllipsisState: TangemTokenRowUM.Content diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt index a4bdecca54..376e670bda 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt @@ -65,9 +65,9 @@ private fun Content( ), ) - when (endContentUM.priceChangeUM) { + when (val priceChangeUM = endContentUM.priceChangeUM) { is PriceChangeState.Content -> TokenRowPriceChangeContent( - priceChangeState = endContentUM.priceChangeUM, + priceChangeState = priceChangeUM, isFlickering = endContentUM.isFlickering, isAvailable = endContentUM.isAvailable, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowSubtitle.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowSubtitle.kt index c0d32baca0..4bcdac0586 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowSubtitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowSubtitle.kt @@ -65,9 +65,9 @@ private fun SubtitleContent(subtitleUM: TangemTokenRowUM.SubtitleUM.Content, mod ), ) - when (subtitleUM.priceChangeUM) { + when (val priceChangeUM = subtitleUM.priceChangeUM) { is PriceChangeState.Content -> TokenRowPriceChangeContent( - priceChangeState = subtitleUM.priceChangeUM, + priceChangeState = priceChangeUM, isFlickering = subtitleUM.isFlickering, isAvailable = subtitleUM.isAvailable, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTitle.kt index ad8368ab4d..65f1334bee 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTitle.kt @@ -21,12 +21,12 @@ import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.ds.badge.TangemBadge import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.extensions.conditional -import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resolveAnnotatedReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @Composable -internal fun TokenRowTitle(titleUM: TangemTokenRowUM.TitleUM, modifier: Modifier = Modifier) { +fun TokenRowTitle(titleUM: TangemTokenRowUM.TitleUM, modifier: Modifier = Modifier) { when (titleUM) { is TangemTokenRowUM.TitleUM.Content -> ContentTitle(titleUM = titleUM, modifier = modifier) TangemTokenRowUM.TitleUM.Loading -> TextShimmer( @@ -42,7 +42,7 @@ internal fun TokenRowTitle(titleUM: TangemTokenRowUM.TitleUM, modifier: Modifier private fun ContentTitle(titleUM: TangemTokenRowUM.TitleUM.Content, modifier: Modifier = Modifier) { Row( modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens2.x4), + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens2.x1), verticalAlignment = Alignment.CenterVertically, ) { /* @@ -50,7 +50,7 @@ private fun ContentTitle(titleUM: TangemTokenRowUM.TitleUM.Content, modifier: Mo * So we need to use [weight] to avoid displacement. */ Text( - text = titleUM.text.resolveReference(), + text = titleUM.text.resolveAnnotatedReference(), modifier = Modifier.weight(weight = 1f, fill = false), color = if (titleUM.isAvailable) { TangemTheme.colors2.text.neutral.primary diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBarInner.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBarInner.kt index 1014c640e4..e445ea4a30 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBarInner.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBarInner.kt @@ -8,6 +8,7 @@ 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.platform.LocalDensity import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.conditionalCompose @@ -37,16 +38,20 @@ internal fun TangemTopBarInner( onEndContentClick: (() -> Unit)? = null, isGhostButtons: Boolean = false, ) { + val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(density = this).toDp() } Box( modifier = modifier - .height(TangemTheme.dimens2.x16) + .height(TangemTheme.dimens2.x16 + statusBarHeight) .fillMaxWidth() + .padding(top = statusBarHeight) .padding(TangemTheme.dimens2.x4, TangemTheme.dimens2.x3), ) { val iconModifier = Modifier .size(TangemTheme.dimens2.x10) .clip(RoundedCornerShape(TangemTheme.dimens2.x25)) - .background(TangemTheme.colors2.button.backgroundSecondary) + .conditionalCompose(isGhostButtons) { + background(TangemTheme.colors2.button.backgroundSecondary) + } AnimatedVisibility( visible = startContent != null, diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index e771427974..1b734bd384 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -384,6 +384,13 @@ fun TextReference.orMaskWithStars(maskWithStars: Boolean): TextReference { return if (maskWithStars) stringReference(THREE_STARS) else this } +/** + * Returns the TextReference itself if it's not null, otherwise returns an empty TextReference. + */ +fun TextReference?.orEmpty(): TextReference { + return this ?: TextReference.EMPTY +} + @ReadOnlyComposable @Composable private fun createStyledText( diff --git a/core/ui/src/main/res/drawable/ic_attention_default_24.xml b/core/ui/src/main/res/drawable/ic_attention_default_24.xml new file mode 100644 index 0000000000..6442020ee2 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_attention_default_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_dollar_default_24.xml b/core/ui/src/main/res/drawable/ic_dollar_default_24.xml new file mode 100644 index 0000000000..d9cc704417 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_dollar_default_24.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_error_sync_default_24.xml b/core/ui/src/main/res/drawable/ic_error_sync_default_24.xml new file mode 100644 index 0000000000..f86403750c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_error_sync_default_24.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_exchange_default_24.xml b/core/ui/src/main/res/drawable/ic_exchange_default_24.xml new file mode 100644 index 0000000000..86260f66f7 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_exchange_default_24.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_filter_default_24.xml b/core/ui/src/main/res/drawable/ic_filter_default_24.xml new file mode 100644 index 0000000000..ec0a6ac12a --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_filter_default_24.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_more_default_24.xml b/core/ui/src/main/res/drawable/ic_more_default_24.xml new file mode 100644 index 0000000000..afdee216b3 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_more_default_24.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_plus_default_24.xml b/core/ui/src/main/res/drawable/ic_plus_default_24.xml new file mode 100644 index 0000000000..cb630c28ca --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_plus_default_24.xml @@ -0,0 +1,13 @@ + + + From 27921d8c3afe2a3e60ca4375a4c0b00009f1a501 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Feb 2026 15:13:10 +0100 Subject: [PATCH 016/106] Updated on 2026-08-14 --- .../api/tangemTech/models/EarnResponse.kt | 1 + .../data/earn/datastore/EarnNetworksStore.kt | 5 +- .../data/earn/datastore/EarnTopTokensStore.kt | 5 +- .../com/tangem/data/earn/di/EarnDataModule.kt | 10 +- .../earn/repository/DefaultEarnRepository.kt | 14 +- .../batch/EarnTokensBatchFetcher.kt | 3 + .../domain/earn/repository/EarnRepository.kt | 4 +- .../earn/usecase/GetEarnNetworksUseCase.kt | 5 +- .../earn/usecase/GetTopEarnTokensUseCase.kt | 2 +- .../models/earn/EarnTokenWithCurrency.kt | 1 + .../AddToPortfolioPreselectedDataComponent.kt | 2 + .../AddToPortfolioPreselectedDataModel.kt | 12 +- .../ui/earn/components/EarnListPlaceholder.kt | 4 +- .../tangem/features/feed/ui/feed/FeedList.kt | 440 +----------------- .../feed/ui/feed/components/BlockHeader.kt | 38 ++ .../ui/feed/components/ChartsFilterChip.kt | 51 ++ .../feed/ui/feed/components/DateBlock.kt | 33 ++ .../feed/ui/feed/components/EarnBlock.kt | 79 ++++ .../feed/ui/feed/components/FeedListError.kt | 30 ++ .../feed/{ => components}/FeedListLoading.kt | 9 +- .../feed/ui/feed/components/MarketsBlock.kt | 176 +++++++ .../feed/ui/feed/components/NewsBlock.kt | 203 ++++++++ .../features/feed/ui/feed/state/EarnListUM.kt | 44 -- 23 files changed, 674 insertions(+), 497 deletions(-) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/ChartsFilterChip.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/DateBlock.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListError.kt rename features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/{ => components}/FeedListLoading.kt (91%) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/EarnListUM.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt index 0e48d4e0f4..47ec19a2ff 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt @@ -24,6 +24,7 @@ data class EarnTokenResponse( @Json(name = "symbol") val symbol: String, @Json(name = "name") val name: String, @Json(name = "address") val address: String, + @Json(name = "decimalCount") val decimalCount: Int? = null, ) @JsonClass(generateAdapter = true) diff --git a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt index ccf38c8902..6bde739432 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt @@ -1,6 +1,5 @@ package com.tangem.data.earn.datastore -import arrow.core.Either import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.domain.models.earn.EarnNetworks import javax.inject.Inject @@ -8,6 +7,6 @@ import javax.inject.Singleton @Singleton class EarnNetworksStore @Inject constructor() : - RuntimeStateStore by RuntimeStateStore( - defaultValue = Either.Right(emptyList()), + RuntimeStateStore by RuntimeStateStore( + defaultValue = null, ) \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt index dded4c2c9b..29f0cbc12e 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt @@ -1,6 +1,5 @@ package com.tangem.data.earn.datastore -import arrow.core.Either import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.domain.models.earn.EarnTopToken import javax.inject.Inject @@ -8,6 +7,6 @@ import javax.inject.Singleton @Singleton class EarnTopTokensStore @Inject constructor() : - RuntimeStateStore by RuntimeStateStore( - defaultValue = Either.Right(emptyList()), + RuntimeStateStore by RuntimeStateStore( + defaultValue = null, ) \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt b/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt index 9cecd0e002..19674a4f95 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt @@ -1,11 +1,11 @@ package com.tangem.data.earn.di -import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.earn.DefaultEarnErrorResolver -import com.tangem.data.earn.repository.DefaultEarnRepository -import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.data.earn.datastore.EarnNetworksStore import com.tangem.data.earn.datastore.EarnTopTokensStore +import com.tangem.data.earn.repository.DefaultEarnRepository +import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.earn.EarnErrorResolver import com.tangem.domain.earn.repository.EarnRepository @@ -29,19 +29,19 @@ internal object EarnDataModule { tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider, userWalletsListRepository: UserWalletsListRepository, - cryptoCurrencyFactory: CryptoCurrencyFactory, earnNetworksStore: EarnNetworksStore, earnTopTokensStore: EarnTopTokensStore, earnErrorResolver: EarnErrorResolver, + excludedBlockchains: ExcludedBlockchains, ): EarnRepository { return DefaultEarnRepository( tangemTechApi = tangemTechApi, dispatchers = dispatchers, userWalletsListRepository = userWalletsListRepository, - cryptoCurrencyFactory = cryptoCurrencyFactory, earnNetworksStore = earnNetworksStore, earnTopTokensStore = earnTopTokensStore, earnErrorResolver = earnErrorResolver, + excludedBlockchains = excludedBlockchains, ) } } \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt b/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt index 98e48ffd57..7a78b887bb 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt @@ -2,6 +2,7 @@ package com.tangem.data.earn.repository import arrow.core.Either import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.earn.converter.EarnTokenConverter @@ -35,12 +36,16 @@ internal class DefaultEarnRepository( private val tangemTechApi: TangemTechApi, private val dispatchers: CoroutineDispatcherProvider, private val userWalletsListRepository: UserWalletsListRepository, - private val cryptoCurrencyFactory: CryptoCurrencyFactory, private val earnNetworksStore: EarnNetworksStore, private val earnTopTokensStore: EarnTopTokensStore, private val earnErrorResolver: EarnErrorResolver, + private val excludedBlockchains: ExcludedBlockchains, ) : EarnRepository { + private val cryptoCurrencyFactory: CryptoCurrencyFactory by lazy { + CryptoCurrencyFactory(excludedBlockchains = excludedBlockchains) + } + override fun getEarnTokensBatchFlow(context: EarnTokensBatchingContext, batchSize: Int): EarnTokensBatchFlow { val batchFetcher = EarnTokensBatchFetcher( tangemTechApi = tangemTechApi, @@ -73,7 +78,7 @@ internal class DefaultEarnRepository( } } - override fun observeEarnNetworks(): Flow { + override fun observeEarnNetworks(): Flow { return earnNetworksStore.get() } @@ -100,6 +105,7 @@ internal class DefaultEarnRepository( EarnTokenWithCurrency( earnToken = earnToken, cryptoCurrency = cryptoCurrency, + networkName = Blockchain.fromNetworkId(dto.networkId)?.fullName.orEmpty(), ) } } @@ -110,7 +116,7 @@ internal class DefaultEarnRepository( } } - override fun observeTopEarnTokens(): Flow { + override fun observeTopEarnTokens(): Flow { return earnTopTokensStore.get() } @@ -150,7 +156,7 @@ internal fun createCryptoCurrencyForEarnToken( rawId = CryptoCurrency.RawID(earnToken.token.id), name = earnToken.token.name, symbol = earnToken.token.symbol, - decimals = blockchain.decimals(), + decimals = earnToken.token.decimalCount ?: blockchain.decimals(), contractAddress = earnToken.token.address, ) } diff --git a/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt b/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt index 0495ea6c6c..85d104f727 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt @@ -1,5 +1,7 @@ package com.tangem.data.earn.repository.batch +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.earn.converter.EarnTokenConverter import com.tangem.data.earn.repository.DefaultEarnRepository.Companion.FIRST_PAGE @@ -88,6 +90,7 @@ internal class EarnTokensBatchFetcher( EarnTokenWithCurrency( earnToken = earnToken, cryptoCurrency = cryptoCurrency, + networkName = Blockchain.fromNetworkId(dto.networkId)?.fullName.orEmpty(), ) } } diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt b/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt index 16d4ad67cb..5727b0cd34 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt @@ -15,12 +15,12 @@ interface EarnRepository { */ suspend fun fetchEarnNetworks(type: String) - fun observeEarnNetworks(): Flow + fun observeEarnNetworks(): Flow /** * Fetch top N earn tokens by isForEarn = true and hold it in runtime store. */ suspend fun fetchTopEarnTokens(limit: Int) - fun observeTopEarnTokens(): Flow + fun observeTopEarnTokens(): Flow } \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt index c5e240bf47..1988d0e166 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt @@ -1,5 +1,6 @@ package com.tangem.domain.earn.usecase +import arrow.core.Either import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.earn.repository.EarnRepository import com.tangem.domain.models.earn.EarnNetwork @@ -27,11 +28,11 @@ class GetEarnNetworksUseCase( earnRepository.observeEarnNetworks(), observeMyNetworkIds(), ) { earn, myNetworkIds -> - earn.map { earnNetworks -> + earn?.map { earnNetworks -> earnNetworks.map { network -> network.copy(isAdded = network.networkId in myNetworkIds) } - } + } ?: Either.Right(emptyList()) }.distinctUntilChanged() } diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetTopEarnTokensUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetTopEarnTokensUseCase.kt index b4a54344f2..65fb391201 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetTopEarnTokensUseCase.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetTopEarnTokensUseCase.kt @@ -9,7 +9,7 @@ class GetTopEarnTokensUseCase( private val repository: EarnRepository, ) { - operator fun invoke(): Flow { + operator fun invoke(): Flow { return repository.observeTopEarnTokens().distinctUntilChanged() } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTokenWithCurrency.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTokenWithCurrency.kt index 5111c260d4..877be4b05a 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTokenWithCurrency.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTokenWithCurrency.kt @@ -11,6 +11,7 @@ import kotlinx.serialization.Serializable */ @Serializable data class EarnTokenWithCurrency( + val networkName: String, val earnToken: EarnToken, val cryptoCurrency: CryptoCurrency, ) \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt index 713ce93cf1..f901b35f54 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.serialization.Serializable interface AddToPortfolioPreselectedDataComponent : ComposableBottomSheetComponent { @@ -22,6 +23,7 @@ interface AddToPortfolioPreselectedDataComponent : ComposableBottomSheetComponen fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId) } + @Serializable data class TokenToAdd( val network: TokenMarketInfo.Network, val id: CryptoCurrency.RawID, 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 b673414488..c6b6d1b45a 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 @@ -112,8 +112,10 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor( // suspend until all required data is selected val (selectedNetworkValue, selectedPortfolioValue) = allRequireForAdd.first() - val isTokenAlreadyAdded = selectedPortfolioValue.account.addedMarketNetworks - .any { it.networkId == selectedNetworkValue.selectedNetwork.networkId } + val isTokenAlreadyAdded = getAccountCurrencyStatusUseCase.invokeSync( + userWalletId = selectedPortfolioValue.userWallet.walletId, + currency = selectedNetworkValue.cryptoCurrency, + ).isSome() if (isTokenAlreadyAdded) { finishSuccessFlow( @@ -165,7 +167,7 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor( ): AvailableToAddData? { val portfolioData = portfolioFetcher.data.firstOrNull() ?: return null - val availableToOpenWallets = portfolioData.balances.mapNotNull { (walletId, balance) -> + val availableToAddInWallets = portfolioData.balances.mapNotNull { (walletId, balance) -> val wallet = balance.userWallet val accounts = balance.accountsBalance.accountStatuses @@ -206,9 +208,9 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor( ) }.toMap() - if (availableToOpenWallets.isEmpty()) return null + if (availableToAddInWallets.isEmpty()) return null - return AvailableToAddData(availableToAddWallets = availableToOpenWallets) + return AvailableToAddData(availableToAddWallets = availableToAddInWallets) } private suspend fun createCryptoCurrency( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListPlaceholder.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListPlaceholder.kt index 87f135e933..0f988ac0bb 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListPlaceholder.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListPlaceholder.kt @@ -17,11 +17,11 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @Composable -internal fun EarnListPlaceholder(modifier: Modifier = Modifier) { +internal fun EarnListPlaceholder(modifier: Modifier = Modifier, placeholderCount: Int = PLACEHOLDER_ITEMS_COUNT) { Column( modifier = modifier.fillMaxSize(), ) { - repeat(PLACEHOLDER_ITEMS_COUNT) { + repeat(placeholderCount) { EarnItemPlaceholder() } } 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 3cb3671be5..27a5de8ffe 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,59 +4,38 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith -import androidx.compose.foundation.* -import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState +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.material3.ripple -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment +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.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.onFirstVisible import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach -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.common.ui.news.ArticleCard -import com.tangem.common.ui.news.ArticleConfigUM -import com.tangem.common.ui.news.ShowMoreArticlesCard 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.UnableToLoadData -import com.tangem.core.ui.components.block.BlockCard -import com.tangem.core.ui.components.block.TangemBlockCardColors -import com.tangem.core.ui.components.buttons.SecondarySmallButton -import com.tangem.core.ui.components.buttons.SmallButtonConfig -import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe 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.feed.components.* import com.tangem.features.feed.ui.feed.preview.FeedListPreviewDataProvider.createFeedPreviewState -import com.tangem.features.feed.ui.feed.state.* +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 @Composable internal fun FeedListHeader( @@ -167,6 +146,13 @@ private fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) { trendingArticle = state.trendingArticle, ) + EarnBlock( + onSeeAllClick = { + // TODO handle in [REDACTED_TASK_KEY] + }, + earnListUM = null, // TODO in [REDACTED_TASK_KEY] + ) + MarketPulseBlock( marketChartConfig = state.marketChartConfig, feedListCallbacks = state.feedListCallbacks, @@ -174,400 +160,6 @@ private fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) { } } -@Composable -private fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedListCallbacks) { - AnimatedContent( - targetState = marketChart, - label = "MarketBlockChartAnimation", - transitionSpec = { fadeIn() togetherWith fadeOut() }, - ) { currentChart -> - when (currentChart) { - is MarketChartUM.Content, - MarketChartUM.Loading, - is MarketChartUM.LoadingError, - -> { - Column(modifier = Modifier.fillMaxWidth()) { - Header( - title = { - Text( - text = stringResourceSafe(R.string.markets_common_title), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) - }, - onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) }, - ) - - SpacerH(12.dp) - - Charts( - onItemClick = feedListCallbacks.onMarketItemClick, - modifier = Modifier.padding(horizontal = 16.dp), - marketChart = currentChart, - ) - - SpacerH(32.dp) - } - } - null -> Unit - } - } -} - -@Composable -private fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) { - val onSeeAllClick by rememberUpdatedState { - feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) - } - if (marketChartConfig.marketCharts.isNotEmpty()) { - Header( - title = { - Text( - text = stringResourceSafe(R.string.markets_pulse_common_title), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) - }, - onSeeAllClick = { onSeeAllClick() }, - ) - - LazyRow( - modifier = Modifier.padding(vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - contentPadding = PaddingValues(16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - state = rememberLazyListState(), - ) { - items( - items = marketChartConfig.getFilterPreset(), - key = SortByTypeUM::name, - ) { sortByTypeUM -> - FilterChip( - sortByTypeUM = sortByTypeUM, - isSelected = sortByTypeUM == marketChartConfig.currentSortByType, - onClick = { feedListCallbacks.onSortTypeClick(sortByTypeUM) }, - ) - } - } - - SpacerH(12.dp) - - AnimatedContent( - targetState = marketChartConfig.currentSortByType, - label = "MarketPulseChartAnimation", - transitionSpec = { fadeIn() togetherWith fadeOut() }, - ) { currentSortType -> - marketChartConfig.marketCharts[currentSortType]?.let { chart -> - Charts( - onItemClick = feedListCallbacks.onMarketItemClick, - modifier = Modifier.padding(horizontal = 16.dp), - marketChart = chart, - ) - } - } - SpacerH(32.dp) - } -} - -@Composable -private fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) { - AnimatedContent(news.newsUMState) { newsUMState -> - when (newsUMState) { - NewsUMState.LOADING -> NewsLoadingBlock() - NewsUMState.CONTENT -> { - if (news.content.isNotEmpty()) { - NewsContentBlock( - feedListCallbacks = feedListCallbacks, - news = news, - trendingArticle = trendingArticle, - ) - } - } - NewsUMState.ERROR -> NewsErrorBlock(onRetryClick = news.onRetryClicked) - } - } -} - -@Suppress("LongMethod") -@Composable -private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) { - val listState = rememberLazyListState() - val articlesReadStatus = remember(news.content) { - news.content.map { it.isViewed } - } - LaunchedEffect(articlesReadStatus) { - listState.requestScrollToItem(0) - } - Column { - Header( - title = { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResourceSafe(R.string.common_news), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) - - SpacerW(4.dp) - - Image( - imageVector = ImageVector.vectorResource(R.drawable.ic_stars_20), - contentDescription = null, - ) - - SpacerW(2.dp) - - Text( - text = buildAnnotatedString { - withStyle( - SpanStyle().copy( - brush = Brush.linearGradient( - GRADIENT_START to LinearGradientFirstPart, - GRADIENT_END to LinearGradientSecondPart, - ), - ), - ) { - append(stringResourceSafe(R.string.feed_tangem_ai)) - } - }, - style = TangemTheme.typography.subtitle1, - ) - } - }, - onSeeAllClick = { feedListCallbacks.onOpenAllNews(false) }, - ) - SpacerH(12.dp) - - if (trendingArticle != null) { - Column { - ArticleCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - articleConfigUM = trendingArticle, - onArticleClick = { feedListCallbacks.onArticleClick(trendingArticle.id) }, - colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), - ) - SpacerH(12.dp) - } - } - - LazyRow( - verticalAlignment = Alignment.CenterVertically, - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - state = listState, - ) { - itemsIndexed( - items = news.content, - key = { _, article -> article.id }, - contentType = { _, _ -> "article" }, - ) { index, article -> - val articleModifier = if (index == FOURTH_ITEM_INDEX) { - Modifier.onFirstVisible( - minFractionVisible = 0.5f, - callback = feedListCallbacks.onSliderScroll, - ) - } else { - Modifier - } - ArticleCard( - articleConfigUM = article, - onArticleClick = { feedListCallbacks.onArticleClick(article.id) }, - modifier = articleModifier - .heightIn(min = 164.dp) - .width(216.dp), - colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), - ) - } - - item(contentType = "show_more") { - ShowMoreArticlesCard( - modifier = Modifier - .width(216.dp) - .heightIn(min = 164.dp) - .onFirstVisible( - minFractionVisible = 0.5f, - callback = feedListCallbacks.onSliderEndReached, - ), - onClick = { feedListCallbacks.onOpenAllNews(true) }, - ) - } - } - SpacerH(32.dp) - } -} - -@Composable -private fun Header(title: @Composable () -> Unit, onSeeAllClick: () -> Unit) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - title() - - SecondarySmallButton( - config = SmallButtonConfig( - text = TextReference.Res(R.string.common_see_all), - onClick = onSeeAllClick, - ), - ) - } -} - -@Composable -private fun Charts( - marketChart: MarketChartUM, - onItemClick: (MarketsListItemUM) -> Unit, - modifier: Modifier = Modifier, -) { - BlockCard( - modifier = modifier, - colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.primary), - ) { - Column(modifier = Modifier.fillMaxWidth()) { - when (marketChart) { - MarketChartUM.Loading -> { - repeat(DEFAULT_CHART_SIZE_IN_MARKET) { - MarketsListItemPlaceholder() - } - } - is MarketChartUM.LoadingError -> { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 35.dp, horizontal = 10.dp), - ) { - UnableToLoadData( - onRetryClick = marketChart.onRetryClicked, - modifier = Modifier.fillMaxWidth(), - ) - } - } - is MarketChartUM.Content -> { - marketChart.items.fastForEach { chart -> - MarketsListItem( - model = chart, - onClick = { onItemClick(chart) }, - ) - } - } - } - } - } -} - -@Composable -private fun FilterChip(sortByTypeUM: SortByTypeUM, isSelected: Boolean, onClick: () -> Unit) { - Box( - modifier = Modifier - .clip(shape = RoundedCornerShape(12.dp)) - .background( - color = if (isSelected) { - TangemTheme.colors.button.primary - } else { - TangemTheme.colors.button.secondary - }, - ) - .clickable( - onClick = onClick, - indication = ripple(), - interactionSource = remember { MutableInteractionSource() }, - ) - .padding(vertical = 8.dp, horizontal = 24.dp), - contentAlignment = Alignment.Center, - ) { - Text( - text = sortByTypeUM.text.resolveReference(), - style = TangemTheme.typography.button, - color = if (isSelected) { - TangemTheme.colors.text.primary2 - } else { - TangemTheme.colors.text.primary1 - }, - ) - } -} - -@Composable -private fun FeedListGlobalError(onRetryClick: () -> Unit, currentDate: String, modifier: Modifier = Modifier) { - val background = LocalMainBottomSheetColor.current.value - Column(modifier) { - DateBlock(currentDate) - Box( - modifier = Modifier - .fillMaxSize() - .drawBehind { drawRect(background) } - .padding(16.dp), - contentAlignment = Alignment.Center, - ) { - UnableToLoadData(onRetryClick = onRetryClick) - } - } -} - -@Composable -private fun NewsErrorBlock(onRetryClick: () -> Unit) { - Column { - Header( - title = { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResourceSafe(R.string.common_news), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) - } - }, - onSeeAllClick = {}, - ) - SpacerH(12.dp) - BlockCard( - modifier = Modifier.padding(horizontal = 16.dp), - colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), - ) { - UnableToLoadData( - onRetryClick = onRetryClick, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 35.dp, horizontal = 10.dp), - ) - } - SpacerH(32.dp) - } -} - -@Composable -private fun ColumnScope.DateBlock(currentDate: String) { - SpacerH(20.dp) - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - text = stringResourceSafe(R.string.feed_market_and_news), - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - ) - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - text = currentDate, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.tertiary, - ) -} - -private const val DEFAULT_CHART_SIZE_IN_MARKET = 5 -private const val FOURTH_ITEM_INDEX = 3 -private const val GRADIENT_START = 0f -private const val GRADIENT_END = 0.5f -private val LinearGradientFirstPart = Color(0xFF635EEC) -private val LinearGradientSecondPart = Color(0xFFE05AED) - @Preview(showBackground = true, heightDp = 1500) @Composable private fun FeedListPreview() { 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 new file mode 100644 index 0000000000..c43c6b6a12 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt @@ -0,0 +1,38 @@ +package com.tangem.features.feed.ui.feed.components + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.extensions.TextReference + +@Composable +internal fun Header(onSeeAllClick: () -> Unit, isLoading: Boolean = false, title: @Composable () -> Unit) { + AnimatedContent(isLoading) { animatedState -> + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + if (animatedState) { + RectangleShimmer(modifier = Modifier.size(width = 104.dp, height = 18.dp)) + } else { + title() + SecondarySmallButton( + config = SmallButtonConfig( + text = TextReference.Res(R.string.common_see_all), + onClick = onSeeAllClick, + ), + ) + } + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/ChartsFilterChip.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/ChartsFilterChip.kt new file mode 100644 index 0000000000..4f36a6b094 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/ChartsFilterChip.kt @@ -0,0 +1,51 @@ +package com.tangem.features.feed.ui.feed.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.model.market.list.state.SortByTypeUM + +@Composable +internal fun ChartsFilterChip(sortByTypeUM: SortByTypeUM, isSelected: Boolean, onClick: () -> Unit) { + Box( + modifier = Modifier + .clip(shape = RoundedCornerShape(12.dp)) + .background( + color = if (isSelected) { + TangemTheme.colors.button.primary + } else { + TangemTheme.colors.button.secondary + }, + ) + .clickable( + onClick = onClick, + indication = ripple(), + interactionSource = remember { MutableInteractionSource() }, + ) + .padding(vertical = 8.dp, horizontal = 24.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = sortByTypeUM.text.resolveReference(), + style = TangemTheme.typography.button, + color = if (isSelected) { + TangemTheme.colors.text.primary2 + } else { + TangemTheme.colors.text.primary1 + }, + ) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..de6399ee68 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/DateBlock.kt @@ -0,0 +1,33 @@ +package com.tangem.features.feed.ui.feed.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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.TangemTheme + +@Composable +internal fun DateBlock(currentDate: String) { + SpacerH(20.dp) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + text = stringResourceSafe(R.string.feed_market_and_news), + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.primary1, + ) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + text = currentDate, + style = TangemTheme.typography.h2, + color = TangemTheme.colors.text.tertiary, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt new file mode 100644 index 0000000000..129ef7438d --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt @@ -0,0 +1,79 @@ +package com.tangem.features.feed.ui.feed.components + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.UnableToLoadData +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.ui.earn.components.EarnListItem +import com.tangem.features.feed.ui.earn.components.EarnListPlaceholder +import com.tangem.features.feed.ui.earn.state.EarnListItemUM +import com.tangem.features.feed.ui.earn.state.EarnListUM +import kotlinx.collections.immutable.ImmutableList + +@Composable +internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modifier: Modifier = Modifier) { + if (earnListUM == null) return + + Column(modifier = modifier) { + Header( + title = { + Text( + text = stringResourceSafe(R.string.markets_earn_common_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + }, + onSeeAllClick = onSeeAllClick, + isLoading = earnListUM is EarnListUM.Loading, + ) + + SpacerH(12.dp) + + BlockCard( + modifier = Modifier.padding(horizontal = 16.dp), + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) { + AnimatedContent(targetState = earnListUM) { earnListState -> + when (earnListState) { + is EarnListUM.Content -> EarnContentBlock(items = earnListState.items) + is EarnListUM.Error -> EarnErrorBlock(onRetryClick = earnListState.onRetryClicked) + EarnListUM.Loading -> EarnListPlaceholder(placeholderCount = PLACEHOLDER_ITEM_COUNT) + } + } + } + SpacerH(32.dp) + } +} + +@Composable +private fun EarnErrorBlock(onRetryClick: () -> Unit) { + UnableToLoadData( + onRetryClick = onRetryClick, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 35.dp, horizontal = 10.dp), + ) +} + +@Composable +private fun EarnContentBlock(items: ImmutableList) { + Column(modifier = Modifier.fillMaxWidth()) { + items.fastForEach { item -> + EarnListItem(item = item) + } + } +} + +private const val PLACEHOLDER_ITEM_COUNT = 5 \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListError.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListError.kt new file mode 100644 index 0000000000..bcf02de5af --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListError.kt @@ -0,0 +1,30 @@ +package com.tangem.features.feed.ui.feed.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.UnableToLoadData +import com.tangem.core.ui.res.LocalMainBottomSheetColor + +@Composable +internal fun FeedListGlobalError(onRetryClick: () -> Unit, currentDate: String, modifier: Modifier = Modifier) { + val background = LocalMainBottomSheetColor.current.value + Column(modifier) { + DateBlock(currentDate) + Box( + modifier = Modifier + .fillMaxSize() + .drawBehind { drawRect(background) } + .padding(16.dp), + contentAlignment = Alignment.Center, + ) { + UnableToLoadData(onRetryClick = onRetryClick) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt similarity index 91% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt index 0788fe844c..1c2798cbf8 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedListLoading.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.ui.feed +package com.tangem.features.feed.ui.feed.components import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow @@ -14,6 +14,8 @@ import com.tangem.common.ui.news.TrendingLoadingArticle import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @Composable @@ -94,7 +96,10 @@ internal fun NewsLoadingBlock() { @Composable private fun ChartsLoading(modifier: Modifier = Modifier) { - BlockCard(modifier) { + BlockCard( + modifier = modifier, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) { Column( modifier = Modifier .fillMaxWidth() 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 new file mode 100644 index 0000000000..1aa00e6976 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt @@ -0,0 +1,176 @@ +package com.tangem.features.feed.ui.feed.components + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +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.UnableToLoadData +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.ui.feed.state.FeedListCallbacks +import com.tangem.features.feed.ui.feed.state.MarketChartConfig +import com.tangem.features.feed.ui.feed.state.MarketChartUM + +@Composable +internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedListCallbacks) { + AnimatedContent( + targetState = marketChart, + label = "MarketBlockChartAnimation", + transitionSpec = { fadeIn() togetherWith fadeOut() }, + ) { currentChart -> + when (currentChart) { + is MarketChartUM.Content, + MarketChartUM.Loading, + is MarketChartUM.LoadingError, + -> { + Column(modifier = Modifier.fillMaxWidth()) { + Header( + title = { + Text( + text = stringResourceSafe(R.string.markets_common_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + }, + onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) }, + ) + + SpacerH(12.dp) + + Charts( + onItemClick = feedListCallbacks.onMarketItemClick, + modifier = Modifier.padding(horizontal = 16.dp), + marketChart = currentChart, + ) + + SpacerH(32.dp) + } + } + null -> Unit + } + } +} + +@Composable +internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCallbacks: FeedListCallbacks) { + val onSeeAllClick by rememberUpdatedState { + feedListCallbacks.onMarketOpenClick(marketChartConfig.currentSortByType) + } + if (marketChartConfig.marketCharts.isNotEmpty()) { + Header( + title = { + Text( + text = stringResourceSafe(R.string.markets_pulse_common_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + }, + onSeeAllClick = { onSeeAllClick() }, + ) + + LazyRow( + modifier = Modifier.padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + state = rememberLazyListState(), + ) { + items( + items = marketChartConfig.getFilterPreset(), + key = SortByTypeUM::name, + ) { sortByTypeUM -> + ChartsFilterChip( + sortByTypeUM = sortByTypeUM, + isSelected = sortByTypeUM == marketChartConfig.currentSortByType, + onClick = { feedListCallbacks.onSortTypeClick(sortByTypeUM) }, + ) + } + } + + SpacerH(12.dp) + + AnimatedContent( + targetState = marketChartConfig.currentSortByType, + label = "MarketPulseChartAnimation", + transitionSpec = { fadeIn() togetherWith fadeOut() }, + ) { currentSortType -> + marketChartConfig.marketCharts[currentSortType]?.let { chart -> + Charts( + onItemClick = feedListCallbacks.onMarketItemClick, + modifier = Modifier.padding(horizontal = 16.dp), + marketChart = chart, + ) + } + } + SpacerH(32.dp) + } +} + +@Composable +private fun Charts( + marketChart: MarketChartUM, + onItemClick: (MarketsListItemUM) -> Unit, + modifier: Modifier = Modifier, +) { + BlockCard( + modifier = modifier, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) { + Column(modifier = Modifier.fillMaxWidth()) { + when (marketChart) { + MarketChartUM.Loading -> { + repeat(DEFAULT_CHART_SIZE_IN_MARKET) { + MarketsListItemPlaceholder() + } + } + is MarketChartUM.LoadingError -> { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 35.dp, horizontal = 10.dp), + ) { + UnableToLoadData( + onRetryClick = marketChart.onRetryClicked, + modifier = Modifier.fillMaxWidth(), + ) + } + } + is MarketChartUM.Content -> { + marketChart.items.fastForEach { chart -> + MarketsListItem( + model = chart, + onClick = { onItemClick(chart) }, + ) + } + } + } + } + } +} + +private const val DEFAULT_CHART_SIZE_IN_MARKET = 5 \ No newline at end of file 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 new file mode 100644 index 0000000000..e2fa4ecbf6 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt @@ -0,0 +1,203 @@ +package com.tangem.features.feed.ui.feed.components + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.onFirstVisible +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.news.ArticleCard +import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.common.ui.news.ShowMoreArticlesCard +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.UnableToLoadData +import com.tangem.core.ui.components.block.BlockCard +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.ui.feed.state.FeedListCallbacks +import com.tangem.features.feed.ui.feed.state.NewsUM +import com.tangem.features.feed.ui.feed.state.NewsUMState + +private const val FOURTH_ITEM_INDEX = 3 +private const val GRADIENT_START = 0f +private const val GRADIENT_END = 0.5f +private val LinearGradientFirstPart = Color(0xFF635EEC) +private val LinearGradientSecondPart = Color(0xFFE05AED) + +@Composable +internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) { + AnimatedContent(news.newsUMState) { newsUMState -> + when (newsUMState) { + NewsUMState.LOADING -> NewsLoadingBlock() + NewsUMState.CONTENT -> { + if (news.content.isNotEmpty()) { + NewsContentBlock( + feedListCallbacks = feedListCallbacks, + news = news, + trendingArticle = trendingArticle, + ) + } + } + NewsUMState.ERROR -> NewsErrorBlock(onRetryClick = news.onRetryClicked) + } + } +} + +@Suppress("LongMethod") +@Composable +private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) { + val listState = rememberLazyListState() + val articlesReadStatus = remember(news.content) { + news.content.map { it.isViewed } + } + LaunchedEffect(articlesReadStatus) { + listState.requestScrollToItem(0) + } + Column { + Header( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResourceSafe(R.string.common_news), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + + SpacerW(4.dp) + + Image( + imageVector = ImageVector.vectorResource(R.drawable.ic_stars_20), + contentDescription = null, + ) + + SpacerW(2.dp) + + Text( + text = buildAnnotatedString { + withStyle( + SpanStyle().copy( + brush = Brush.linearGradient( + GRADIENT_START to LinearGradientFirstPart, + GRADIENT_END to LinearGradientSecondPart, + ), + ), + ) { + append(stringResourceSafe(R.string.feed_tangem_ai)) + } + }, + style = TangemTheme.typography.subtitle1, + ) + } + }, + onSeeAllClick = { feedListCallbacks.onOpenAllNews(false) }, + ) + SpacerH(12.dp) + + if (trendingArticle != null) { + Column { + ArticleCard( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + articleConfigUM = trendingArticle, + onArticleClick = { feedListCallbacks.onArticleClick(trendingArticle.id) }, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) + SpacerH(12.dp) + } + } + + LazyRow( + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + state = listState, + ) { + itemsIndexed( + items = news.content, + key = { _, article -> article.id }, + contentType = { _, _ -> "article" }, + ) { index, article -> + val articleModifier = if (index == FOURTH_ITEM_INDEX) { + Modifier.onFirstVisible( + minFractionVisible = 0.5f, + callback = feedListCallbacks.onSliderScroll, + ) + } else { + Modifier + } + ArticleCard( + articleConfigUM = article, + onArticleClick = { feedListCallbacks.onArticleClick(article.id) }, + modifier = articleModifier + .heightIn(min = 164.dp) + .width(216.dp), + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) + } + + item(contentType = "show_more") { + ShowMoreArticlesCard( + modifier = Modifier + .width(216.dp) + .heightIn(min = 164.dp) + .onFirstVisible( + minFractionVisible = 0.5f, + callback = feedListCallbacks.onSliderEndReached, + ), + onClick = { feedListCallbacks.onOpenAllNews(true) }, + ) + } + } + SpacerH(32.dp) + } +} + +@Composable +private fun NewsErrorBlock(onRetryClick: () -> Unit) { + Column { + Header( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResourceSafe(R.string.common_news), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) + } + }, + onSeeAllClick = {}, + ) + SpacerH(12.dp) + BlockCard( + modifier = Modifier.padding(horizontal = 16.dp), + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) { + UnableToLoadData( + onRetryClick = onRetryClick, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 35.dp, horizontal = 10.dp), + ) + } + SpacerH(32.dp) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/EarnListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/EarnListUM.kt deleted file mode 100644 index f703b958c0..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/EarnListUM.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.features.feed.ui.feed.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.serialization.SerializedBigDecimal -import kotlinx.collections.immutable.ImmutableList - -internal data class EarnListUM( - val items: ImmutableList, - val contentState: EarnListContentState, -) - -@Immutable -internal data class EarnListItemUM( - val network: TextReference.Str, - val symbol: String, - val tokenName: String, - val currencyIconState: CurrencyIconState, - val earnValue: EarnValueUM, - val earnType: EarnType, - val onItemClick: () -> Unit, -) - -@Immutable -internal data class EarnValueUM( - val percent: SerializedBigDecimal, - val earnValueType: EarnValueType, -) - -internal enum class EarnType { - Staking, Yield -} - -internal enum class EarnValueType { - APR, APY -} - -@Immutable -internal sealed interface EarnListContentState { - data object Loading : EarnListContentState - data object Content : EarnListContentState - data class Error(val onRetryClicked: () -> Unit) : EarnListContentState -} \ No newline at end of file From 8ba59403f4eddd62642b3db69ca1ba6150451104 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Feb 2026 07:35:28 +0100 Subject: [PATCH 017/106] Updated on 2026-08-14 --- .../api/tangemTech/TangemTechApi.kt | 2 +- .../data/earn/DefaultEarnErrorResolver.kt | 2 +- .../data/earn/converter/EarnTokenConverter.kt | 17 +- .../tangem/domain/models/earn/EarnError.kt | 7 +- .../tangem/domain/models/earn/EarnToken.kt | 15 +- features/feed/impl/build.gradle.kts | 1 + .../components/DefaultFeedEntryComponent.kt | 9 + .../feed/components/FeedEntryChildFactory.kt | 3 + .../components/feed/DefaultFeedComponent.kt | 31 +++ .../components/feed/FeedPortfolioRoute.kt | 11 ++ .../impl/di/AddToPortfolioComponentModule.kt | 7 + .../feed/model/feed/FeedComponentModel.kt | 180 +++++++++++++----- .../feed/model/feed/FeedModelClickIntents.kt | 2 + .../model/feed/state/FeedStateController.kt | 19 +- .../UpdateEarnLoadingStateTransformer.kt | 11 ++ .../UpdateEarnStateTransformer.kt | 111 +++++++++++ .../UpdateGlobalFeedStateTransformer.kt | 93 ++++++--- .../tangem/features/feed/ui/feed/FeedList.kt | 6 +- .../preview/FeedListPreviewDataProvider.kt | 30 +++ .../features/feed/ui/feed/state/FeedListUM.kt | 3 + 20 files changed, 463 insertions(+), 97 deletions(-) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedPortfolioRoute.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnLoadingStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt 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 7dad440994..0b0731ea4d 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 @@ -207,7 +207,7 @@ interface TangemTechApi { suspend fun transactionEvents(@Body name: TransactionEventBody): ApiResponse // region Earn - @GET("v1/earn/yield/markets") + @GET("v1/earn/markets") suspend fun getEarnTokens( @Query("isForEarn") isForEarn: Boolean?, @Query("page") page: String? = null, diff --git a/data/earn/src/main/java/com/tangem/data/earn/DefaultEarnErrorResolver.kt b/data/earn/src/main/java/com/tangem/data/earn/DefaultEarnErrorResolver.kt index 1b1d416a64..52b9499bb1 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/DefaultEarnErrorResolver.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/DefaultEarnErrorResolver.kt @@ -14,7 +14,7 @@ internal class DefaultEarnErrorResolver : EarnErrorResolver { message = throwable.message.orEmpty(), ) } - else -> EarnError.NotHttpError + else -> EarnError.NotHttpError() } } } \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/converter/EarnTokenConverter.kt b/data/earn/src/main/java/com/tangem/data/earn/converter/EarnTokenConverter.kt index eb33d52cb2..6dd7dc4c6e 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/converter/EarnTokenConverter.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/converter/EarnTokenConverter.kt @@ -1,7 +1,9 @@ package com.tangem.data.earn.converter import com.tangem.datasource.api.tangemTech.models.EarnResponse +import com.tangem.domain.models.earn.EarnRewardType import com.tangem.domain.models.earn.EarnToken +import com.tangem.domain.models.earn.EarnType import com.tangem.utils.converter.Converter internal object EarnTokenConverter : Converter { @@ -10,12 +12,23 @@ internal object EarnTokenConverter : Converter { return EarnToken( apy = value.apy, networkId = value.networkId, - rewardType = value.rewardType, - type = value.type, + rewardType = convertEarnRewardType(value.rewardType), + type = convertEarnType(value.type), tokenId = value.token.id, tokenSymbol = value.token.symbol, tokenName = value.token.name, tokenAddress = value.token.address, + decimalCount = value.token.decimalCount, ) } + + private fun convertEarnRewardType(type: String): EarnRewardType { + return enumValues().find { it.name.equals(type, ignoreCase = true) } + ?: EarnRewardType.APY + } + + private fun convertEarnType(type: String): EarnType { + return enumValues().find { it.name.equals(type, ignoreCase = true) } + ?: EarnType.STAKING + } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnError.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnError.kt index d5106276e8..880febc265 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnError.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnError.kt @@ -1,11 +1,16 @@ package com.tangem.domain.models.earn +import java.util.UUID + sealed interface EarnError { data class HttpError( + val id: String = UUID.randomUUID().toString(), val code: Int, val message: String, ) : EarnError - data object NotHttpError : EarnError + data class NotHttpError( + val id: String = UUID.randomUUID().toString(), + ) : EarnError } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt index a91581e8eb..523ba3ac45 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt @@ -17,10 +17,19 @@ import kotlinx.serialization.Serializable data class EarnToken( val apy: String, val networkId: String, - val rewardType: String, - val type: String, + val rewardType: EarnRewardType, + val type: EarnType, val tokenId: String, val tokenSymbol: String, val tokenName: String, val tokenAddress: String, -) \ No newline at end of file + val decimalCount: Int?, +) + +enum class EarnRewardType { + APR, APY +} + +enum class EarnType { + STAKING, YIELD +} \ No newline at end of file diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts index e16f11c113..beb8db8937 100644 --- a/features/feed/impl/build.gradle.kts +++ b/features/feed/impl/build.gradle.kts @@ -54,6 +54,7 @@ dependencies { implementation(projects.domain.news) implementation(projects.domain.yieldSupply.models) implementation(projects.domain.yieldSupply) + implementation(projects.domain.earn) // FIXME [REDACTED_TASK_KEY] // Remove the "Buy" and "Sell" actions from the redux middleware. 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 edbc89d7d4..9b8a539048 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,6 +22,7 @@ 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 @@ -131,6 +132,14 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( override fun onOpenAllNews() { innerRouter.push(FeedEntryChildFactory.Child.NewsList) } + + override fun onOpenEarnPage() { + innerRouter.push( + FeedEntryChildFactory.Child.Earn( + params = DefaultEarnComponent.Params(onBackClick = { onChildBack() }), + ), + ) + } } private val stack: Value> = 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 3ecec193e2..9deae9964b 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 @@ -10,6 +10,7 @@ import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.features.feed.components.earn.DefaultEarnComponent import com.tangem.features.feed.components.feed.DefaultFeedComponent 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 @@ -21,6 +22,7 @@ internal class FeedEntryChildFactory @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val accountsFeatureToggles: AccountsFeatureToggles, private val portfolioComponentFactory: MarketsPortfolioComponent.Factory, + private val addToPortfolioPreselectedDataComponent: AddToPortfolioPreselectedDataComponent.Factory, ) { @Serializable @@ -100,6 +102,7 @@ internal class FeedEntryChildFactory @Inject constructor( DefaultFeedComponent( appComponentContext = appComponentContext, params = DefaultFeedComponent.FeedParams(feedClickIntents = feedEntryClickIntents), + addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent, ) } is Child.Earn -> { 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 f7073e2ca2..d7f9b477fa 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 @@ -6,10 +6,16 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.LifecycleStartEffect 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.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.model.feed.FeedComponentModel import com.tangem.features.feed.model.feed.FeedModelClickIntents import com.tangem.features.feed.ui.feed.FeedList @@ -18,10 +24,18 @@ import com.tangem.features.feed.ui.feed.FeedListHeader internal class DefaultFeedComponent( appComponentContext: AppComponentContext, private val params: FeedParams, + private val addToPortfolioComponentFactory: AddToPortfolioPreselectedDataComponent.Factory, ) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext { private val feedComponentModel = getOrCreateModel(params = params) + private val bottomSheetSlot = childSlot( + source = feedComponentModel.bottomSheetNavigation, + serializer = FeedPortfolioRoute.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + @Composable override fun Title(bottomSheetState: State) { val state by feedComponentModel.state.collectAsStateWithLifecycle() @@ -40,11 +54,28 @@ internal class DefaultFeedComponent( } } + val bottomSheet by bottomSheetSlot.subscribeAsState() val state by feedComponentModel.state.collectAsStateWithLifecycle() FeedList( modifier = modifier, state = state, ) + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + config: FeedPortfolioRoute, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = when (config) { + is FeedPortfolioRoute.AddToPortfolio -> { + addToPortfolioComponentFactory.create( + context = childByContext(componentContext), + params = AddToPortfolioPreselectedDataComponent.Params( + tokenToAdd = config.tokenToAdd, + callback = feedComponentModel.addToPortfolioCallback, + ), + ) + } } data class FeedParams(val feedClickIntents: FeedModelClickIntents) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedPortfolioRoute.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedPortfolioRoute.kt new file mode 100644 index 0000000000..afaf4193dd --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedPortfolioRoute.kt @@ -0,0 +1,11 @@ +package com.tangem.features.feed.components.feed + +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent +import kotlinx.serialization.Serializable + +@Serializable +sealed interface FeedPortfolioRoute { + + @Serializable + data class AddToPortfolio(val tokenToAdd: AddToPortfolioPreselectedDataComponent.TokenToAdd) : FeedPortfolioRoute +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioComponentModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioComponentModule.kt index b522e1b6e3..1233c2be6a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioComponentModule.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioComponentModule.kt @@ -2,7 +2,9 @@ package com.tangem.features.feed.components.market.details.portfolio.add.impl.di 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.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.components.market.details.portfolio.add.impl.DefaultAddToPortfolioComponent +import com.tangem.features.feed.components.market.details.portfolio.add.impl.DefaultAddToPortfolioPreselectedDataComponent import com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.DefaultAddToPortfolioManager import dagger.Binds import dagger.Module @@ -18,4 +20,9 @@ internal interface AddToPortfolioComponentModule { @Binds fun bindAddToPortfolioManagerFactory(factory: DefaultAddToPortfolioManager.Factory): AddToPortfolioManager.Factory + + @Binds + fun bindAddToPortfolioPreselectedDataComponent( + factory: DefaultAddToPortfolioPreselectedDataComponent.Factory, + ): AddToPortfolioPreselectedDataComponent.Factory } \ 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 8c1774fcf8..c5cbabead3 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 @@ -2,6 +2,11 @@ package com.tangem.features.feed.model.feed import androidx.compose.runtime.Stable import arrow.core.getOrElse +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -12,20 +17,28 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.earn.usecase.FetchTopEarnTokensUseCase +import com.tangem.domain.earn.usecase.GetTopEarnTokensUseCase import com.tangem.domain.markets.GetTopFiveMarketTokenUseCase +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.earn.EarnTokenWithCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase import com.tangem.features.feed.components.feed.DefaultFeedComponent +import com.tangem.features.feed.components.feed.FeedPortfolioRoute +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent +import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.feed.impl.R import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent import com.tangem.features.feed.model.feed.state.FeedMarketsBatchFlowManager import com.tangem.features.feed.model.feed.state.FeedStateController -import com.tangem.features.feed.model.feed.state.transformers.UpdateGlobalFeedStateTransformer -import com.tangem.features.feed.model.feed.state.transformers.UpdateMarketChartsTransformer -import com.tangem.features.feed.model.feed.state.transformers.UpdateTrendingNewsStateTransformer +import com.tangem.features.feed.model.feed.state.transformers.* import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.ui.earn.state.EarnListUM import com.tangem.features.feed.ui.feed.state.* import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -41,22 +54,26 @@ import javax.inject.Inject @Stable @ModelScoped -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class FeedComponentModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase, private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val stateController: FeedStateController, + private val feedFeatureToggle: FeedFeatureToggle, + private val fetchTopEarnTokensUseCase: FetchTopEarnTokensUseCase, + private val getTopEarnTokensUseCase: GetTopEarnTokensUseCase, + private val appRouter: AppRouter, getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, paramsContainer: ParamsContainer, ) : Model() { - private val params = paramsContainer.require() - private var quotesUpdateJob: Job? = null + private val params = paramsContainer.require() + private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } }.stateIn( @@ -72,7 +89,22 @@ internal class FeedComponentModel @Inject constructor( dispatchers = dispatchers, ) - internal val state: StateFlow + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + + val addToPortfolioCallback = object : AddToPortfolioPreselectedDataComponent.Callback { + override fun onDismiss() = bottomSheetNavigation.dismiss() + override fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId) { + bottomSheetNavigation.dismiss() + appRouter.push( + AppRoute.CurrencyDetails( + userWalletId = walletId, + currency = addedToken, + ), + ) + } + } + + val state: StateFlow get() = stateController.uiState val isVisibleOnScreen = MutableStateFlow(false) @@ -81,29 +113,38 @@ internal class FeedComponentModel @Inject constructor( initializeState() updateCallbacks() fetchTrendingNews() + fetchEarnData() + fetchCharts() subscribeOnCurrencyUpdate() - loadCharts() + subscribeOnDataState() + } + + private fun subscribeOnDataState() { modelScope.launch(dispatchers.default) { combine( flow = marketsBatchFlowManager.itemsByOrder, flow2 = marketsBatchFlowManager.loadingStatesByOrder, flow3 = marketsBatchFlowManager.errorStatesByOrder, flow4 = manageTrendingNewsUseCase.observeTrendingNews(), - ) { itemsByOrder, loadingStatesByOrder, errorStatesByOrder, trendingNewsResult -> + flow5 = getTopEarnTokensUseCase(), + ) { itemsByOrder, loadingStatesByOrder, errorStatesByOrder, trendingNewsResult, earnResult -> val globalStateTransformer = UpdateGlobalFeedStateTransformer( loadingStatesByOrder = loadingStatesByOrder, errorStatesByOrder = errorStatesByOrder, trendingNewsResult = trendingNewsResult, + earnResult = earnResult, onRetryClicked = { modelScope.launch(dispatchers.default) { stateController.update { currentState -> currentState.copy(globalState = GlobalFeedState.Loading) } - fetchTrendingNewsUseCase.invoke() + fetchTrendingNews() + fetchEarnData() marketsBatchFlowManager.reloadAll() } }, analyticsEventHandler = analyticsEventHandler, + feedFeatureToggle = feedFeatureToggle, ) val currentState = stateController.value @@ -123,13 +164,15 @@ internal class FeedComponentModel @Inject constructor( ), UpdateTrendingNewsStateTransformer( result = trendingNewsResult, - onRetryClicked = { - modelScope.launch(dispatchers.default) { - fetchTrendingNewsUseCase.invoke() - } - }, + onRetryClicked = ::fetchTrendingNews, analyticsEventHandler = analyticsEventHandler, ), + UpdateEarnStateTransformer( + isEarnEnabled = feedFeatureToggle.isEarnBlockEnabled, + onItemClick = ::handleEarnTokenClick, + onRetryClick = ::fetchEarnData, + earnResult = earnResult, + ), ) } @@ -144,12 +187,6 @@ internal class FeedComponentModel @Inject constructor( } } - private fun fetchTrendingNews() { - modelScope.launch(dispatchers.default) { - fetchTrendingNewsUseCase() - } - } - private fun subscribeOnCurrencyUpdate() { modelScope.launch(dispatchers.default) { currentAppCurrency.drop(1).collect { @@ -158,7 +195,21 @@ internal class FeedComponentModel @Inject constructor( } } - private fun loadCharts() { + private fun fetchTrendingNews() { + modelScope.launch(dispatchers.default) { + fetchTrendingNewsUseCase() + } + } + + private fun fetchEarnData() { + if (!feedFeatureToggle.isEarnBlockEnabled) return + modelScope.launch(dispatchers.default) { + stateController.update(UpdateEarnLoadingStateTransformer()) + fetchTopEarnTokensUseCase() + } + } + + private fun fetchCharts() { modelScope.launch(dispatchers.default) { TokenMarketListConfig.Order.entries.forEach { order -> marketsBatchFlowManager.getOnLastBatchLoadedSuccessFlow(order)?.collect { batchKey -> @@ -198,6 +249,7 @@ internal class FeedComponentModel @Inject constructor( onSliderEndReached = { analyticsEventHandler.send(FeedAnalyticsEvent.NewsCarouselEndReached()) }, + onOpenEarnPageClick = params.feedClickIntents::onOpenEarnPage, ), news = NewsUM( content = persistentListOf(), @@ -214,14 +266,41 @@ internal class FeedComponentModel @Inject constructor( currentSortByType = SortByTypeUM.TopGainers, ), globalState = GlobalFeedState.Loading, + earnListUM = if (feedFeatureToggle.isEarnBlockEnabled) { + EarnListUM.Loading + } else { + null + }, ) } - private fun getCurrentDate(): String { - val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault()) - return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate) + private fun startQuotesUpdateTimer() { + quotesUpdateJob?.cancel() + quotesUpdateJob = modelScope.launch { + while (true) { + delay(DELAY_TO_FETCH_QUOTES) + isVisibleOnScreen.first { it } + marketsBatchFlowManager.updateQuotes() + } + } } + private fun updateCallbacks() { + stateController.update { feedListUM -> + feedListUM.copy( + feedListCallbacks = feedListUM.feedListCallbacks.copy( + onSortTypeClick = ::handleSortTypeClicked, + onMarketItemClick = ::handleMarketItemClicked, + onMarketOpenClick = ::handleMarketOpenClicked, + onArticleClick = ::handleArticleClicked, + onOpenAllNews = ::handleOpenAllNews, + onOpenEarnPageClick = params.feedClickIntents::onOpenEarnPage, + ), + ) + } + } + + /* start of clicks area */ private fun handleSortTypeClicked(sortByType: SortByTypeUM) { stateController.update { currentState -> val updatedCharts = currentState.marketChartConfig.marketCharts.mapValues { (chartSortType, chart) -> @@ -249,31 +328,6 @@ internal class FeedComponentModel @Inject constructor( } } - private fun startQuotesUpdateTimer() { - quotesUpdateJob?.cancel() - quotesUpdateJob = modelScope.launch { - while (true) { - delay(DELAY_TO_FETCH_QUOTES) - isVisibleOnScreen.first { it } - marketsBatchFlowManager.updateQuotes() - } - } - } - - private fun updateCallbacks() { - stateController.update { feedListUM -> - feedListUM.copy( - feedListCallbacks = feedListUM.feedListCallbacks.copy( - onSortTypeClick = ::handleSortTypeClicked, - onMarketItemClick = ::handleMarketItemClicked, - onMarketOpenClick = ::handleMarketOpenClicked, - onArticleClick = ::handleArticleClicked, - onOpenAllNews = ::handleOpenAllNews, - ), - ) - } - } - private fun handleMarketItemClicked(item: MarketsListItemUM) { val tokenMarket = marketsBatchFlowManager.getTokenMarketById(item.id) if (tokenMarket != null) { @@ -342,6 +396,26 @@ internal class FeedComponentModel @Inject constructor( params.feedClickIntents.onOpenAllNews() } + private fun handleEarnTokenClick(earnTokenWithCurrency: EarnTokenWithCurrency) { + bottomSheetNavigation.activate( + FeedPortfolioRoute.AddToPortfolio( + tokenToAdd = AddToPortfolioPreselectedDataComponent.TokenToAdd( + network = TokenMarketInfo.Network( + networkId = earnTokenWithCurrency.earnToken.networkId, + isExchangeable = false, + contractAddress = earnTokenWithCurrency.earnToken.tokenAddress, + decimalCount = earnTokenWithCurrency.earnToken.decimalCount, + ), + id = CryptoCurrency.RawID(earnTokenWithCurrency.earnToken.tokenId), + name = earnTokenWithCurrency.earnToken.tokenName, + symbol = earnTokenWithCurrency.earnToken.tokenSymbol, + ), + ), + ) + } + /* end of clicks area */ + + /* start of utils area */ private fun SortByTypeUM.toOrder(): TokenMarketListConfig.Order { return when (this) { SortByTypeUM.Rating -> TokenMarketListConfig.Order.ByRating @@ -354,6 +428,12 @@ internal class FeedComponentModel @Inject constructor( } } + private fun getCurrentDate(): String { + val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault()) + return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate) + } + /* end of utils area */ + companion object { private const val DELAY_TO_FETCH_QUOTES = 60_000L } 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 d3ade33796..b9778a2ed6 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 @@ -27,4 +27,6 @@ internal interface FeedModelClickIntents { paginationConfig: NewsListConfig? = null, ) fun onOpenAllNews() + + fun onOpenEarnPage() } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt index 161b83222b..99440c5949 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt @@ -1,8 +1,11 @@ package com.tangem.features.feed.model.feed.state import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.feed.model.feed.state.transformers.FeedListUMTransformer import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.ui.earn.state.EarnListUM import com.tangem.features.feed.ui.feed.state.* import kotlinx.collections.immutable.persistentHashMapOf import kotlinx.collections.immutable.persistentListOf @@ -13,7 +16,9 @@ import kotlinx.coroutines.flow.update import javax.inject.Inject @ModelScoped -internal class FeedStateController @Inject constructor() { +internal class FeedStateController @Inject constructor( + private val feedFeatureToggle: FeedFeatureToggle, +) { private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState()) @@ -33,15 +38,11 @@ internal class FeedStateController @Inject constructor() { transformers.forEach { mutableUiState.update(function = it::transform) } } - fun clear() { - mutableUiState.update { getInitialState() } - } - private fun getInitialState(): FeedListUM { return FeedListUM( currentDate = "", feedListSearchBar = FeedListSearchBar( - placeholderText = com.tangem.core.ui.extensions.TextReference.EMPTY, + placeholderText = TextReference.EMPTY, onBarClick = {}, ), feedListCallbacks = FeedListCallbacks( @@ -53,6 +54,7 @@ internal class FeedStateController @Inject constructor() { onSortTypeClick = {}, onSliderScroll = {}, onSliderEndReached = {}, + onOpenEarnPageClick = {}, ), news = NewsUM( content = persistentListOf(), @@ -65,6 +67,11 @@ internal class FeedStateController @Inject constructor() { currentSortByType = SortByTypeUM.Trending, ), globalState = GlobalFeedState.Loading, + earnListUM = if (feedFeatureToggle.isEarnBlockEnabled) { + EarnListUM.Loading + } else { + null + }, ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnLoadingStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnLoadingStateTransformer.kt new file mode 100644 index 0000000000..22a796a576 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnLoadingStateTransformer.kt @@ -0,0 +1,11 @@ +package com.tangem.features.feed.model.feed.state.transformers + +import com.tangem.features.feed.ui.earn.state.EarnListUM +import com.tangem.features.feed.ui.feed.state.FeedListUM + +internal class UpdateEarnLoadingStateTransformer : FeedListUMTransformer { + + override fun transform(prevState: FeedListUM): FeedListUM { + return prevState.copy(earnListUM = EarnListUM.Loading) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt new file mode 100644 index 0000000000..bf3daee21c --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt @@ -0,0 +1,111 @@ +package com.tangem.features.feed.model.feed.state.transformers + +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.domain.models.earn.EarnRewardType +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.domain.models.earn.EarnTopToken +import com.tangem.domain.models.earn.EarnType +import com.tangem.features.feed.impl.R +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.state.FeedListUM +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +internal class UpdateEarnStateTransformer( + private val isEarnEnabled: Boolean, + private val onItemClick: (EarnTokenWithCurrency) -> Unit, + private val onRetryClick: () -> Unit, + private val earnResult: EarnTopToken?, +) : FeedListUMTransformer { + + private val earnTokenWithCurrencyConverter = EarnTokenWithCurrencyConverter(onItemClick = onItemClick) + + override fun transform(prevState: FeedListUM): FeedListUM { + if (!isEarnEnabled) return handleEmptyState(prevState) + return when (earnResult) { + null -> { + handleEmptyState(prevState) + } + else -> { + earnResult.fold( + ifLeft = { + handleErrorState(prevState) + }, + ifRight = { earnTokensWithCurrency -> + handleDataState( + currentState = prevState, + earnTokensWithCurrency = earnTokensWithCurrency, + ) + }, + ) + } + } + } + + private fun handleEmptyState(currentState: FeedListUM): FeedListUM { + return currentState.copy(earnListUM = null) + } + + private fun handleErrorState(currentState: FeedListUM): FeedListUM { + return currentState.copy(earnListUM = EarnListUM.Error(onRetryClick)) + } + + private fun handleDataState( + currentState: FeedListUM, + earnTokensWithCurrency: List, + ): FeedListUM { + return currentState.copy( + earnListUM = EarnListUM.Content( + items = earnTokensWithCurrency + .sortedWith( + compareByDescending { + it.earnToken.apy.toBigDecimalOrNull() ?: BigDecimal.ZERO + }.thenBy { it.earnToken.tokenName }, + ) + .map { earnToken -> + earnTokenWithCurrencyConverter.convert(earnToken) + } + .toPersistentList(), + ), + ) + } + + private class EarnTokenWithCurrencyConverter( + private val onItemClick: (EarnTokenWithCurrency) -> Unit, + ) : Converter { + override fun convert(value: EarnTokenWithCurrency): EarnListItemUM { + return EarnListItemUM( + network = TextReference.Str(value.networkName), + symbol = TextReference.Str(value.earnToken.tokenSymbol), + tokenName = TextReference.Str(value.earnToken.tokenName), + currencyIconState = CryptoCurrencyToIconStateConverter().convert(value.cryptoCurrency), + earnValue = when (value.earnToken.rewardType) { + EarnRewardType.APR -> TextReference.Res( + id = R.string.staking_apr_earn_badge, + formatArgs = wrappedList(convertPercent(value.earnToken.apy)), + ) + EarnRewardType.APY -> TextReference.Res( + id = R.string.yield_module_earn_badge, + formatArgs = wrappedList(convertPercent(value.earnToken.apy)), + ) + }, + earnType = when (value.earnToken.type) { + EarnType.STAKING -> TextReference.Res(R.string.common_staking) + EarnType.YIELD -> TextReference.Res(R.string.common_yield_mode) + }, + onItemClick = { onItemClick(value) }, + ) + } + + private fun convertPercent(value: String): TextReference { + val percent = BigDecimal(value).format { percent(withPercentSign = false) } + return TextReference.Str(percent) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt index dc78f2b7a0..56d9d140a4 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt @@ -1,57 +1,58 @@ package com.tangem.features.feed.model.feed.state.transformers import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.models.earn.EarnTopToken import com.tangem.domain.models.news.TrendingNews +import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.ui.earn.state.EarnListUM import com.tangem.features.feed.ui.feed.state.FeedListUM import com.tangem.features.feed.ui.feed.state.GlobalFeedState import com.tangem.features.feed.ui.feed.state.NewsUMState +@Suppress("LongParameterList") internal class UpdateGlobalFeedStateTransformer( private val loadingStatesByOrder: Map, private val errorStatesByOrder: Map, private val trendingNewsResult: TrendingNews, + private val earnResult: EarnTopToken?, private val onRetryClicked: () -> Unit, private val analyticsEventHandler: AnalyticsEventHandler, + private val feedFeatureToggle: FeedFeatureToggle, ) : FeedListUMTransformer { - @Suppress("CyclomaticComplexMethod") override fun transform(prevState: FeedListUM): FeedListUM { - val previousGlobalState = prevState.globalState + val blockStates = buildList { + add(getNewsState(prevState)) + add(getChartsState()) + if (feedFeatureToggle.isEarnBlockEnabled) { + add(getEarnState(prevState)) + } + } - val isNewsLoading = prevState.news.newsUMState == NewsUMState.LOADING - val isNewsError = trendingNewsResult is TrendingNews.Error || - prevState.news.newsUMState == NewsUMState.ERROR + val newGlobalState = when { + blockStates.all { it == BlockState.ERROR } -> { + if (prevState.globalState !is GlobalFeedState.Error) { + sendErrorAnalytics() + } + GlobalFeedState.Error(onRetryClicked = onRetryClicked) + } + blockStates.any { it == BlockState.LOADING } -> GlobalFeedState.Loading + else -> GlobalFeedState.Content + } + return prevState.copy(globalState = newGlobalState) + } + + private fun areAllChartsError(): Boolean { val hasLoadingInCharts = loadingStatesByOrder.values.any { it } val hasErrorInMarketChart = errorStatesByOrder[SortByTypeUM.Rating] != null val hasErrorInMarketPulseChart = errorStatesByOrder .any { it.key != SortByTypeUM.Rating && it.value != null } - - val areAllChartsLoading = loadingStatesByOrder.values.isNotEmpty() && loadingStatesByOrder.values.all { it } - val areAllChartsError = when { - hasErrorInMarketChart && hasErrorInMarketPulseChart -> true - hasErrorInMarketPulseChart && hasLoadingInCharts -> true - hasErrorInMarketChart && hasLoadingInCharts -> true - else -> false - } - - val newGlobalState = when { - isNewsError && areAllChartsLoading -> GlobalFeedState.Loading - isNewsError && areAllChartsError -> { - if (previousGlobalState !is GlobalFeedState.Error) { - sendErrorAnalytics() - } - GlobalFeedState.Error( - onRetryClicked = onRetryClicked, - ) - } - isNewsLoading && areAllChartsError -> GlobalFeedState.Loading - isNewsLoading && areAllChartsLoading -> GlobalFeedState.Loading - else -> GlobalFeedState.Content - } - return prevState.copy(globalState = newGlobalState) + return hasErrorInMarketChart && hasErrorInMarketPulseChart || + hasErrorInMarketPulseChart && hasLoadingInCharts || + hasErrorInMarketChart && hasLoadingInCharts } private fun sendErrorAnalytics() { @@ -59,4 +60,38 @@ internal class UpdateGlobalFeedStateTransformer( FeedAnalyticsEvent.AllWidgetsLoadError(), ) } + + private fun getNewsState(prevState: FeedListUM): BlockState { + val isError = trendingNewsResult is TrendingNews.Error || prevState.news.newsUMState == NewsUMState.ERROR + if (isError) return BlockState.ERROR + + val isLoading = prevState.news.newsUMState == NewsUMState.LOADING && trendingNewsResult !is TrendingNews.Data + if (isLoading) return BlockState.LOADING + + return BlockState.CONTENT + } + + private fun getChartsState(): BlockState { + if (areAllChartsError()) return BlockState.ERROR + + val isLoading = loadingStatesByOrder.values.isNotEmpty() && loadingStatesByOrder.values.all { it } + if (isLoading) return BlockState.LOADING + + return BlockState.CONTENT + } + + private fun getEarnState(prevState: FeedListUM): BlockState { + val isError = when (earnResult) { + null -> false + else -> earnResult.isLeft() + } + if (isError) return BlockState.ERROR + + val isLoading = prevState.earnListUM is EarnListUM.Loading && earnResult == null + if (isLoading) return BlockState.LOADING + + return BlockState.CONTENT + } + + private enum class BlockState { LOADING, ERROR, CONTENT } } \ 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 27a5de8ffe..943dd2fce3 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 @@ -147,10 +147,8 @@ private fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) { ) EarnBlock( - onSeeAllClick = { - // TODO handle in [REDACTED_TASK_KEY] - }, - earnListUM = null, // TODO in [REDACTED_TASK_KEY] + onSeeAllClick = state.feedListCallbacks.onOpenEarnPageClick, + earnListUM = state.earnListUM, ) MarketPulseBlock( 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 62d38e2004..ceaf16a400 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 @@ -3,13 +3,18 @@ package com.tangem.features.feed.ui.feed.preview import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.core.ui.R +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM import com.tangem.core.ui.components.label.entity.LabelUM 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.res.TangemColorPalette import com.tangem.domain.models.currency.CryptoCurrency 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.state.* import kotlinx.collections.immutable.* @@ -34,6 +39,7 @@ internal object FeedListPreviewDataProvider { onSortTypeClick = {}, onSliderScroll = {}, onSliderEndReached = {}, + onOpenEarnPageClick = {}, ), news = NewsUM( content = articles.filter { it.isTrending.not() }.toImmutableList(), @@ -45,6 +51,9 @@ internal object FeedListPreviewDataProvider { marketCharts = createMarketCharts(marketItems, includeErrorState = false), currentSortByType = SortByTypeUM.TopGainers, ), + earnListUM = EarnListUM.Content( + items = createEarnListItemsUM(), + ), ) } @@ -250,4 +259,25 @@ internal object FeedListPreviewDataProvider { updateTimestamp = 0, ) } + + private fun createEarnListItemsUM(): ImmutableList { + return List(5) { + EarnListItemUM( + network = stringReference("Ethereum"), + symbol = stringReference("USDT"), + tokenName = stringReference("TETHER"), + currencyIconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_eth_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + earnValue = stringReference("APY 6.54%"), + earnType = stringReference("Yield"), + onItemClick = {}, + ) + }.toPersistentList() + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt index d9c50773a0..8d8a014175 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt @@ -5,6 +5,7 @@ import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.common.ui.news.ArticleConfigUM import com.tangem.core.ui.extensions.TextReference import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.ui.earn.state.EarnListUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentListOf @@ -17,6 +18,7 @@ internal data class FeedListUM( val trendingArticle: ArticleConfigUM?, val marketChartConfig: MarketChartConfig, val globalState: GlobalFeedState = GlobalFeedState.Content, + val earnListUM: EarnListUM?, ) internal data class FeedListCallbacks( @@ -28,6 +30,7 @@ internal data class FeedListCallbacks( val onSortTypeClick: (sortBy: SortByTypeUM) -> Unit, val onSliderScroll: () -> Unit, val onSliderEndReached: () -> Unit, + val onOpenEarnPageClick: () -> Unit, ) internal data class FeedListSearchBar( From 18d4a01642fd09cbe513757aa8fa0cc703cc0d52 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Feb 2026 11:13:31 +0400 Subject: [PATCH 018/106] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 8 ++++ .../DefaultUserWalletsListRepository.kt | 43 +++++++++++++++++ .../UserWalletsPublicInformationRepository.kt | 4 ++ ...tUserWalletsPublicInformationRepository.kt | 15 ++++++ .../wallets/UserWalletTransformAction.kt | 35 ++++++++++++++ .../wallets/UserWalletsListRepository.kt | 8 ++++ .../ApplyUserWalletListSortingUseCase.kt | 46 +++++++++++++++++++ 7 files changed, 159 insertions(+) create mode 100644 domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletTransformAction.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ApplyUserWalletListSortingUseCase.kt 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 6e58009dd5..b3c453e6c5 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 @@ -398,4 +398,12 @@ internal object WalletsDomainModule { fun provideSyncWalletWithRemoteUseCase(walletsRepository: WalletsRepository): SyncWalletWithRemoteUseCase { return SyncWalletWithRemoteUseCase(walletsRepository = walletsRepository) } + + @Provides + @Singleton + fun provideApplyUserWalletListSortingUseCase( + userWalletsListRepository: UserWalletsListRepository, + ): ApplyUserWalletListSortingUseCase { + return ApplyUserWalletListSortingUseCase(userWalletsListRepository = userWalletsListRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index 414ea8a0b3..f307b8653d 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -13,6 +13,7 @@ import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.domain.common.wallets.UserWalletTransformAction import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.UserWalletsListRepository.LockMethod import com.tangem.domain.common.wallets.error.* @@ -31,6 +32,8 @@ import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey import com.tangem.tap.domain.userWalletList.utils.encryptionKey import com.tangem.tap.domain.userWalletList.utils.lock +import com.tangem.tap.domain.userWalletList.utils.publicInformation +import com.tangem.tap.domain.userWalletList.utils.sensitiveInformation import com.tangem.tap.domain.userWalletList.utils.toUserWallets import com.tangem.tap.domain.userWalletList.utils.updateWith import com.tangem.utils.Provider @@ -38,10 +41,12 @@ import com.tangem.utils.ProviderSuspend import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.extensions.addOrReplace import com.tangem.utils.extensions.indexOfFirstOrNull +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext @Suppress("LongParameterList", "LargeClass") internal class DefaultUserWalletsListRepository( @@ -384,6 +389,44 @@ internal class DefaultUserWalletsListRepository( return userWallets.any { it.walletId !in unsecuredWalletIds } } + override suspend fun transform(action: UserWalletTransformAction) { + val wallets = userWalletsSync() + val walletsMap = wallets.associateBy { it.walletId } + val transformedWallets = action.transform(wallets) + val transformedWalletsMap = transformedWallets.associateBy { it.walletId } + + require(walletsMap.keys == transformedWalletsMap.keys) { + "The transformation action must not change the set of wallet IDs." + + "Original IDs: ${walletsMap.keys}, Transformed IDs: ${transformedWalletsMap.keys}" + } + + updateWallets { transformedWallets } + + withContext(NonCancellable) { + if (savePersistentInformation()) { + publicInformationRepository.transform { + transformedWallets.map { wallet -> wallet.publicInformation } + } + + val changedSensitiveInfoWallets = wallets.filter { wallet -> + val transformedWallet = transformedWalletsMap[wallet.walletId] + transformedWallet != null && transformedWallet.sensitiveInformation != wallet.sensitiveInformation + } + + changedSensitiveInfoWallets.forEach { wallet -> + if (wallet.isLocked.not()) { + sensitiveInformationRepository.save(wallet, wallet.encryptionKey) + } + + checkForUpgradeAndDeleteHotWalletIfNeeded( + newUserWallet = wallet, + oldUserWallet = walletsMap[wallet.walletId] ?: error("This should never happen"), + ) + } + } + } + } + private suspend fun checkForUpgradeAndDeleteHotWalletIfNeeded( newUserWallet: UserWallet, oldUserWallet: UserWallet, diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsPublicInformationRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsPublicInformationRepository.kt index 6daa482e36..2497e8224a 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsPublicInformationRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsPublicInformationRepository.kt @@ -12,4 +12,8 @@ internal interface UserWalletsPublicInformationRepository { suspend fun delete(walletIds: List): CompletionResult suspend fun clear(): CompletionResult + + suspend fun transform( + block: (List?) -> List?, + ): CompletionResult } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt index c3e8f86072..f2e3f0fcea 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/DefaultUserWalletsPublicInformationRepository.kt @@ -77,6 +77,21 @@ internal class DefaultUserWalletsPublicInformationRepository( } } + override suspend fun transform( + block: (List?) -> List?, + ): CompletionResult { + return withContext(Dispatchers.IO) { + getAll().flatMap { currentInfo -> + val transformed = block(currentInfo) + if (transformed != null) { + save(transformed) + } else { + CompletionResult.Success(Unit) + } + } + } + } + @JvmName("saveWithPublicInformation") private suspend fun save(publicInformation: List): CompletionResult = catching { withContext(Dispatchers.IO) { diff --git a/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletTransformAction.kt b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletTransformAction.kt new file mode 100644 index 0000000000..ddf77dce94 --- /dev/null +++ b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletTransformAction.kt @@ -0,0 +1,35 @@ +package com.tangem.domain.common.wallets + +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Sealed class representing actions that can transform the user wallets list. + */ +sealed class UserWalletTransformAction { + + /** + * The transformation block to apply to the current wallets list. + */ + abstract fun transform(list: List): List + + /** + * Reorders user wallets + * + * @param action The transformation block that takes the current list of user wallets + * and returns a list of user wallet IDs in the desired order. + */ + data class Reorder( + private val action: (List) -> List, + ) : UserWalletTransformAction() { + + override fun transform(list: List): List { + val walletById = list.associateBy { it.walletId } + val newOrderIds = action(list) + require(walletById.keys == newOrderIds.toSet()) { + "Reorder action must return the same set of wallet IDs as the input list" + } + return newOrderIds.mapNotNull { walletById[it] } + } + } +} \ No newline at end of file diff --git a/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt index 52df2eb43d..c9f9aecb00 100644 --- a/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt +++ b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt @@ -123,6 +123,14 @@ interface UserWalletsListRepository { */ suspend fun clearPersistentData() + /** + * Transforms user wallets list according to the provided action. + * + * @param action The transformation action to apply. + * @throws IllegalArgumentException if the transformation action is invalid (e.g. reordering with missing or extra wallet IDs). + */ + suspend fun transform(action: UserWalletTransformAction) + /** * Checks if there are any secured wallets (wallets that are not locked with [LockMethod.NoLock]). */ diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ApplyUserWalletListSortingUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ApplyUserWalletListSortingUseCase.kt new file mode 100644 index 0000000000..4e3ac597d4 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ApplyUserWalletListSortingUseCase.kt @@ -0,0 +1,46 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.common.wallets.UserWalletTransformAction +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Use case to apply a specific sorting order to a list of user wallets. + * + * @property userWalletsListRepository Repository for managing user wallets list. + */ +class ApplyUserWalletListSortingUseCase( + private val userWalletsListRepository: UserWalletsListRepository, +) { + + /** + * Applies the sorting order of the provided list of user wallet IDs. + * + * @param userWalletIds List of [UserWalletId] representing the desired order. + * @return Either an [Error] or Unit on successful completion. + */ + suspend operator fun invoke(userWalletIds: List): Either = either { + ensure(userWalletIds.size > 1) { Error.UnableToSortSingleWallet } + catch( + block = { + userWalletsListRepository.transform(UserWalletTransformAction.Reorder { userWalletIds }) + }, + catch = { raise(Error.ReorderError) }, + ) + } + + /** + * Sealed interface representing possible errors that can occur during the application + * of user wallet list sorting. + */ + sealed interface Error { + + data object UnableToSortSingleWallet : Error + + data object ReorderError : Error + } +} \ No newline at end of file From bced1ca48d8199192b12b0c8767dc887bcf3f45a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Feb 2026 12:39:07 +0500 Subject: [PATCH 019/106] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 6 + .../features/feed/model/earn/EarnModel.kt | 228 ++++++++++++- .../features/feed/ui/earn/EarnContent.kt | 306 ++++++++++++------ .../EarnFilterByNetworkBottomSheet.kt | 245 ++++++++++++++ .../components/EarnFilterByTypeBottomSheet.kt | 85 +++++ ...EarnFilterByNetworkBottomSheetContentUM.kt | 10 + .../EarnFilterByTypeBottomSheetContentUM.kt | 8 + .../feed/ui/earn/state/EarnFilterNetworkUM.kt | 29 ++ .../feed/ui/earn/state/EarnFilterTypeUM.kt | 11 + .../features/feed/ui/earn/state/EarnListUM.kt | 9 + .../features/feed/ui/earn/state/EarnUM.kt | 27 +- 11 files changed, 845 insertions(+), 119 deletions(-) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByTypeBottomSheet.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByNetworkBottomSheetContentUM.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByTypeBottomSheetContentUM.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterTypeUM.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index ed8c132108..ae10eb765b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -469,7 +469,13 @@ Send funds using only Best opportunities All networks + My networks All types + Networks + Clear filter + Filter by + No results + The list is temporarily empty as it’s being refreshed. Check back in a moment. Mostly used Earn Hi support team, I\'ve encountered an error with code: %s 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 f1471cf802..2717a8708d 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 @@ -4,13 +4,24 @@ import androidx.compose.runtime.Stable import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.ui.R +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.features.feed.components.earn.DefaultEarnComponent +import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM +import com.tangem.features.feed.ui.earn.state.EarnFilterByNetworkBottomSheetContentUM +import com.tangem.features.feed.ui.earn.state.EarnFilterByTypeBottomSheetContentUM +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM import com.tangem.features.feed.ui.earn.state.EarnListUM import com.tangem.features.feed.ui.earn.state.EarnUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import javax.inject.Inject @Stable @@ -22,18 +33,213 @@ internal class EarnModel @Inject constructor( private val params = paramsContainer.require() + // TODO [REDACTED_TASK_KEY] remove mocks + private val mockNetworks: ImmutableList = persistentListOf( + EarnFilterNetworkUM.AllNetworks( + text = resourceReference(R.string.earn_filter_all_networks), + isSelected = false, + ), + EarnFilterNetworkUM.MyNetworks( + text = resourceReference(R.string.earn_filter_my_networks), + isSelected = false, + ), + EarnFilterNetworkUM.Network( + id = "ethereum", + text = stringReference("Ethereum"), + symbol = stringReference("ETH"), + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/ethereum.png", + isSelected = false, + ), + EarnFilterNetworkUM.Network( + id = "polygon", + text = stringReference("Polygon"), + symbol = stringReference("MATIC"), + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/matic-network.png", + isSelected = false, + ), + EarnFilterNetworkUM.Network( + id = "base", + text = stringReference("Base"), + symbol = stringReference("BASE"), + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/base.png", + isSelected = false, + ), + EarnFilterNetworkUM.Network( + id = "avalanche", + text = stringReference("Avalance"), + symbol = stringReference("AVAX"), + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/avalanche-2.png", + isSelected = false, + ), + EarnFilterNetworkUM.Network( + id = "cardano", + text = stringReference("Cardano"), + symbol = stringReference("ADA"), + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/cardano.png", + isSelected = false, + ), + EarnFilterNetworkUM.Network( + id = "bsc", + text = stringReference("Bitcoin Smart Chain"), + symbol = stringReference("BSC"), + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/binancecoin.png", + isSelected = false, + ), + EarnFilterNetworkUM.Network( + id = "bitcoin", + text = stringReference("Bitcoin"), + symbol = stringReference("BTC"), + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/bitcoin.png", + isSelected = false, + ), + EarnFilterNetworkUM.Network( + id = "solana", + text = stringReference("Solana"), + symbol = stringReference("SOL"), + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/solana.png", + isSelected = false, + ), + EarnFilterNetworkUM.Network( + id = "ton", + text = stringReference("Ton"), + symbol = stringReference("TON"), + iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/the-open-network.png", + isSelected = false, + ), + ) + val state: StateFlow field = MutableStateFlow(createInitialState()) - private fun createInitialState(): EarnUM = EarnUM( - mostlyUsed = EarnListUM.Loading, - bestOpportunities = EarnListUM.Loading, - selectedNetworkFilter = null, - selectedTypeFilter = null, - networkFilters = persistentListOf(), - typeFilters = persistentListOf(), - onBackClick = params.onBackClick, - onNetworkFilterClick = { }, - onTypeFilterClick = { }, - ) + private fun createInitialState(): EarnUM { + val defaultNetworkFilter = mockNetworks.first() + val defaultTypeFilter = EarnFilterTypeUM.All + + return EarnUM( + mostlyUsed = EarnListUM.Loading, + bestOpportunities = EarnBestOpportunitiesUM.Loading, + selectedTypeFilter = defaultTypeFilter, + selectedNetworkFilter = defaultNetworkFilter, + filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(defaultTypeFilter), + filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig(defaultNetworkFilter), + onBackClick = params.onBackClick, + onNetworkFilterClick = ::onNetworkFilterClick, + onTypeFilterClick = ::onTypeFilterClick, + ) + } + + private fun createTypeFilterBottomSheetConfig( + selectedOption: EarnFilterTypeUM, + isShown: Boolean = false, + ): TangemBottomSheetConfig { + return TangemBottomSheetConfig( + isShown = isShown, + onDismissRequest = ::hideTypeFilterBottomSheet, + content = EarnFilterByTypeBottomSheetContentUM( + selectedOption = selectedOption, + onOptionClick = ::onTypeFilterOptionClick, + ), + ) + } + + private fun createNetworkFilterBottomSheetConfig( + selectedNetwork: EarnFilterNetworkUM, + isShown: Boolean = false, + ): TangemBottomSheetConfig { + val updatedNetworks = mockNetworks.map { network -> + when { + network is EarnFilterNetworkUM.AllNetworks && selectedNetwork is EarnFilterNetworkUM.AllNetworks -> + network.copy(isSelected = true) + network is EarnFilterNetworkUM.MyNetworks && selectedNetwork is EarnFilterNetworkUM.MyNetworks -> + network.copy(isSelected = true) + network is EarnFilterNetworkUM.Network && selectedNetwork is EarnFilterNetworkUM.Network && + network.id == selectedNetwork.id -> + network.copy(isSelected = true) + else -> network + } + } + + return TangemBottomSheetConfig( + isShown = isShown, + onDismissRequest = ::hideNetworkFilterBottomSheet, + content = EarnFilterByNetworkBottomSheetContentUM( + selectedNetwork = selectedNetwork, + networks = persistentListOf(*updatedNetworks.toTypedArray()), + onOptionClick = ::onNetworkFilterOptionClick, + ), + ) + } + + private fun onTypeFilterClick() { + state.update { currentState -> + currentState.copy( + filterByTypeBottomSheet = createTypeFilterBottomSheetConfig( + selectedOption = currentState.selectedTypeFilter, + isShown = true, + ), + ) + } + } + + private fun onNetworkFilterClick() { + state.update { currentState -> + currentState.copy( + filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig( + selectedNetwork = currentState.selectedNetworkFilter, + isShown = true, + ), + ) + } + } + + private fun hideTypeFilterBottomSheet() { + state.update { currentState -> + currentState.copy( + filterByTypeBottomSheet = currentState.filterByTypeBottomSheet.copy(isShown = false), + ) + } + } + + private fun hideNetworkFilterBottomSheet() { + state.update { currentState -> + currentState.copy( + filterByNetworkBottomSheet = currentState.filterByNetworkBottomSheet.copy(isShown = false), + ) + } + } + + private fun onTypeFilterOptionClick(type: EarnFilterTypeUM) { + state.update { currentState -> + currentState.copy( + selectedTypeFilter = type, + filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(selectedOption = type), + ) + } + } + + private fun onNetworkFilterOptionClick(network: EarnFilterNetworkUM) { + state.update { currentState -> + currentState.copy( + selectedNetworkFilter = network, + filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig(selectedNetwork = network), + ) + } + } + + // TODO [REDACTED_TASK_KEY] remove + @Suppress("UnusedPrivateMember") + private fun onClearFiltersClick() { + val defaultNetworkFilter = mockNetworks.first() + val defaultTypeFilter = EarnFilterTypeUM.All + state.update { currentState -> + currentState.copy( + selectedTypeFilter = defaultTypeFilter, + selectedNetworkFilter = defaultNetworkFilter, + filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(selectedOption = defaultTypeFilter), + filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig( + selectedNetwork = defaultNetworkFilter, + ), + ) + } + } } \ 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 657d7c312d..63753d95a1 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 @@ -1,6 +1,7 @@ package com.tangem.features.feed.ui.earn import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -9,12 +10,16 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.R @@ -37,10 +42,16 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.feed.ui.earn.components.EarnFilterByNetworkBottomSheet +import com.tangem.features.feed.ui.earn.components.EarnFilterByTypeBottomSheet import com.tangem.features.feed.ui.earn.components.EarnItemPlaceholder import com.tangem.features.feed.ui.earn.components.EarnListItem import com.tangem.features.feed.ui.earn.components.MostlyUsedPlaceholder -import com.tangem.features.feed.ui.earn.state.EarnFilterUM +import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM import com.tangem.features.feed.ui.earn.state.EarnListItemUM import com.tangem.features.feed.ui.earn.state.EarnListUM import com.tangem.features.feed.ui.earn.state.EarnUM @@ -51,6 +62,10 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { val background = LocalMainBottomSheetColor.current.value val density = LocalDensity.current val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() } + + EarnFilterByTypeBottomSheet(config = state.filterByTypeBottomSheet) + EarnFilterByNetworkBottomSheet(config = state.filterByNetworkBottomSheet) + LazyColumn( modifier = modifier .fillMaxSize() @@ -79,8 +94,8 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { SpacerH(12.dp) BestOpportunitiesFilters( state = state.bestOpportunities, - selectedNetworkFilter = state.selectedNetworkFilter, - selectedTypeFilter = state.selectedTypeFilter, + selectedNetworkFilterText = state.selectedNetworkFilterText, + selectedTypeFilterText = state.selectedTypeFilterText, onNetworkFilterClick = state.onNetworkFilterClick, onTypeFilterClick = state.onTypeFilterClick, ) @@ -94,40 +109,42 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { @Composable private fun MostlyUsedContent(state: EarnListUM) { - when (state) { - is EarnListUM.Loading -> { - MostlyUsedPlaceholder() - } - is EarnListUM.Content -> { - LazyRow( - contentPadding = PaddingValues( - horizontal = 16.dp, - vertical = 12.dp, - ), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - items( - items = state.items, - key = { "${it.tokenName}-${it.network}" }, - ) { item -> - MostlyUsedCard( - item = item, - onClick = item.onItemClick, - ) - } + AnimatedContent(state) { st -> + when (st) { + is EarnListUM.Loading -> { + MostlyUsedPlaceholder() } - } - is EarnListUM.Error -> { - Box( - modifier = Modifier - .fillMaxWidth() - .padding( + is EarnListUM.Content -> { + LazyRow( + contentPadding = PaddingValues( horizontal = 16.dp, vertical = 12.dp, ), - contentAlignment = Alignment.Center, - ) { - UnableToLoadData(onRetryClick = state.onRetryClicked) + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items( + items = st.items, + key = { "${it.tokenName}-${it.network}" }, + ) { item -> + MostlyUsedCard( + item = item, + onClick = item.onItemClick, + ) + } + } + } + is EarnListUM.Error -> { + Box( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 12.dp, + ), + contentAlignment = Alignment.Center, + ) { + UnableToLoadData(onRetryClick = st.onRetryClicked) + } } } } @@ -184,40 +201,46 @@ private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: @Composable private fun BestOpportunitiesFilters( - state: EarnListUM, - selectedNetworkFilter: EarnFilterUM?, - selectedTypeFilter: EarnFilterUM?, + state: EarnBestOpportunitiesUM, + selectedNetworkFilterText: TextReference, + selectedTypeFilterText: TextReference, onNetworkFilterClick: () -> Unit, onTypeFilterClick: () -> Unit, ) { - when (state) { - is EarnListUM.Loading -> { - FilterButtonsShimmer() - } - is EarnListUM.Content -> { - FilterButtons( - selectedNetworkFilter = selectedNetworkFilter, - selectedTypeFilter = selectedTypeFilter, - isEnabled = true, - onNetworkFilterClick = onNetworkFilterClick, - onTypeFilterClick = onTypeFilterClick, - ) - } - is EarnListUM.Error -> { - FilterButtons( - selectedNetworkFilter = selectedNetworkFilter, - selectedTypeFilter = selectedTypeFilter, - isEnabled = false, - onNetworkFilterClick = onNetworkFilterClick, - onTypeFilterClick = onTypeFilterClick, - ) + AnimatedContent(state) { st -> + when (st) { + is EarnBestOpportunitiesUM.Loading -> { + FilterButtonsShimmer() + } + is EarnBestOpportunitiesUM.EmptyFiltered, + is EarnBestOpportunitiesUM.Content, + -> { + FilterButtons( + selectedNetworkFilterText = selectedNetworkFilterText, + selectedTypeFilterText = selectedTypeFilterText, + isEnabled = true, + onNetworkFilterClick = onNetworkFilterClick, + onTypeFilterClick = onTypeFilterClick, + ) + } + is EarnBestOpportunitiesUM.Empty, + is EarnBestOpportunitiesUM.Error, + -> { + FilterButtons( + selectedNetworkFilterText = selectedNetworkFilterText, + selectedTypeFilterText = selectedTypeFilterText, + isEnabled = false, + onNetworkFilterClick = onNetworkFilterClick, + onTypeFilterClick = onTypeFilterClick, + ) + } } } } -private fun LazyListScope.bestOpportunitiesItems(state: EarnListUM) { +private fun LazyListScope.bestOpportunitiesItems(state: EarnBestOpportunitiesUM) { when (state) { - is EarnListUM.Loading -> { + is EarnBestOpportunitiesUM.Loading -> { val lastIndex = PLACEHOLDER_ITEMS_COUNT - 1 items( count = PLACEHOLDER_ITEMS_COUNT, @@ -233,7 +256,19 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnListUM) { ) } } - is EarnListUM.Content -> { + is EarnBestOpportunitiesUM.Empty -> { + item(key = "best_opportunities_empty") { + SpacerH(12.dp) + BestOpportunitiesEmpty() + } + } + is EarnBestOpportunitiesUM.EmptyFiltered -> { + item(key = "best_opportunities_empty_filtered") { + SpacerH(12.dp) + BestOpportunitiesEmptyFiltered(onClearFilterClick = state.onClearFilterClick) + } + } + is EarnBestOpportunitiesUM.Content -> { if (state.items.isNotEmpty()) { val lastIndex = state.items.lastIndex itemsIndexed( @@ -252,7 +287,7 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnListUM) { } } } - is EarnListUM.Error -> { + is EarnBestOpportunitiesUM.Error -> { item(key = "best_opportunities_error") { SpacerH(12.dp) Box( @@ -275,8 +310,8 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnListUM) { @Composable private fun FilterButtons( - selectedNetworkFilter: EarnFilterUM?, - selectedTypeFilter: EarnFilterUM?, + selectedNetworkFilterText: TextReference, + selectedTypeFilterText: TextReference, isEnabled: Boolean, onNetworkFilterClick: () -> Unit, onTypeFilterClick: () -> Unit, @@ -287,8 +322,7 @@ private fun FilterButtons( ) { SecondarySmallButton( config = SmallButtonConfig( - text = selectedNetworkFilter?.name - ?: resourceReference(R.string.earn_filter_all_networks), + text = selectedNetworkFilterText, onClick = onNetworkFilterClick, icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24), isEnabled = isEnabled, @@ -299,8 +333,7 @@ private fun FilterButtons( SecondarySmallButton( config = SmallButtonConfig( - text = selectedTypeFilter?.name - ?: resourceReference(R.string.earn_filter_all_types), + text = selectedTypeFilterText, onClick = onTypeFilterClick, icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24), isEnabled = isEnabled, @@ -326,6 +359,66 @@ private fun FilterButtonsShimmer(modifier: Modifier = Modifier) { } } +@Composable +private fun BestOpportunitiesEmpty(modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding(vertical = 32.dp, horizontal = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier + .fillMaxWidth(), + painter = painterResource(R.drawable.ic_empty_64), + contentDescription = null, + tint = Color.Unspecified, + ) + SpacerH(24.dp) + Text( + modifier = Modifier + .padding(horizontal = 32.dp), + text = stringResourceSafe(R.string.earn_empty), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ) + } +} + +@Composable +private fun BestOpportunitiesEmptyFiltered(onClearFilterClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding(vertical = 32.dp, horizontal = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResourceSafe(R.string.earn_no_results), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + SpacerH(12.dp) + SecondarySmallButton( + config = SmallButtonConfig( + text = resourceReference(R.string.earn_clear_filter), + onClick = onClearFilterClick, + ), + ) + } +} + @Composable private fun SectionHeader(title: String, modifier: Modifier = Modifier) { Text( @@ -348,7 +441,7 @@ private fun EarnContentPreview() { LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, ) { EarnContent( - state = EarnUM( + state = previewEarnUM( mostlyUsed = EarnListUM.Content( items = persistentListOf( previewEarnListItemUM(), @@ -359,7 +452,7 @@ private fun EarnContentPreview() { ), ), ), - bestOpportunities = EarnListUM.Content( + bestOpportunities = EarnBestOpportunitiesUM.Content( items = persistentListOf( previewEarnListItemUM( tokenName = "Cosmos Hub", @@ -373,13 +466,6 @@ private fun EarnContentPreview() { ), ), ), - selectedNetworkFilter = null, - selectedTypeFilter = null, - networkFilters = persistentListOf(), - typeFilters = persistentListOf(), - onBackClick = {}, - onNetworkFilterClick = {}, - onTypeFilterClick = {}, ), ) } @@ -396,16 +482,9 @@ private fun EarnContentLoadingPreview() { LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, ) { EarnContent( - state = EarnUM( + state = previewEarnUM( mostlyUsed = EarnListUM.Loading, - bestOpportunities = EarnListUM.Loading, - selectedNetworkFilter = null, - selectedTypeFilter = null, - networkFilters = persistentListOf(), - typeFilters = persistentListOf(), - onBackClick = {}, - onNetworkFilterClick = {}, - onTypeFilterClick = {}, + bestOpportunities = EarnBestOpportunitiesUM.Loading, ), ) } @@ -422,7 +501,7 @@ private fun EarnContentErrorPreview() { LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, ) { EarnContent( - state = EarnUM( + state = previewEarnUM( mostlyUsed = EarnListUM.Content( items = persistentListOf( previewEarnListItemUM(), @@ -433,14 +512,35 @@ private fun EarnContentErrorPreview() { ), ), ), - bestOpportunities = EarnListUM.Error(onRetryClicked = {}), - selectedNetworkFilter = null, - selectedTypeFilter = null, - networkFilters = persistentListOf(), - typeFilters = persistentListOf(), - onBackClick = {}, - onNetworkFilterClick = {}, - onTypeFilterClick = {}, + bestOpportunities = EarnBestOpportunitiesUM.Error(onRetryClicked = {}), + ), + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun EarnContentEmptyPreview() { + TangemThemePreview { + val background = TangemTheme.colors.background.tertiary + CompositionLocalProvider( + LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, + ) { + EarnContent( + state = previewEarnUM( + mostlyUsed = EarnListUM.Content( + items = persistentListOf( + previewEarnListItemUM(), + previewEarnListItemUM( + tokenName = "Cosmos", + symbol = "ATOM", + network = "Cosmos", + ), + ), + ), + bestOpportunities = EarnBestOpportunitiesUM.Empty, ), ) } @@ -469,4 +569,22 @@ private fun previewEarnListItemUM( onItemClick = {}, ) +private fun previewEarnUM( + mostlyUsed: EarnListUM = EarnListUM.Loading, + bestOpportunities: EarnBestOpportunitiesUM = EarnBestOpportunitiesUM.Loading, +): EarnUM = EarnUM( + mostlyUsed = mostlyUsed, + bestOpportunities = bestOpportunities, + selectedTypeFilter = EarnFilterTypeUM.All, + selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks( + text = resourceReference(R.string.earn_filter_all_networks), + isSelected = true, + ), + filterByTypeBottomSheet = TangemBottomSheetConfig.Empty, + filterByNetworkBottomSheet = TangemBottomSheetConfig.Empty, + onBackClick = {}, + onNetworkFilterClick = {}, + onTypeFilterClick = {}, +) + private const val PLACEHOLDER_ITEMS_COUNT = 8 \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt new file mode 100644 index 0000000000..6b31102b92 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt @@ -0,0 +1,245 @@ +package com.tangem.features.feed.ui.earn.components + +import android.content.res.Configuration +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.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.components.currency.icon.CoinIcon +import com.tangem.core.ui.components.inputrow.InputRowChecked +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.components.rows.RowContentContainer +import com.tangem.core.ui.components.rows.RowText +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +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.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.feed.impl.R +import com.tangem.features.feed.ui.earn.state.EarnFilterByNetworkBottomSheetContentUM +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun EarnFilterByNetworkBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + titleText = resourceReference(R.string.earn_filter_by), + containerColor = TangemTheme.colors.background.tertiary, + content = { Content(it) }, + ) +} + +@Composable +private fun Content(content: EarnFilterByNetworkBottomSheetContentUM) { + val allMyNetworks = remember(content) { + content.networks.filterIsInstance() + + content.networks.filterIsInstance() + } + val specificNetworks = remember(content) { content.networks.filterIsInstance() } + + LazyColumn( + contentPadding = PaddingValues( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + allMyNetworksList( + allMyNetworks = allMyNetworks, + onOptionClicked = content.onOptionClick, + ) + + if (specificNetworks.isNotEmpty()) { + networksHeader() + + specificNetworksList( + specificNetworks = specificNetworks, + onOptionClicked = content.onOptionClick, + ) + } + } +} + +private fun LazyListScope.allMyNetworksList( + allMyNetworks: List, + onOptionClicked: (EarnFilterNetworkUM) -> Unit, +) { + itemsIndexed( + items = allMyNetworks, + key = { _, item -> + when (item) { + is EarnFilterNetworkUM.AllNetworks -> "all_networks" + is EarnFilterNetworkUM.MyNetworks -> "my_networks" + is EarnFilterNetworkUM.Network -> item.id + } + }, + ) { index, item -> + DividerContainer( + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = allMyNetworks.lastIndex, + addDefaultPadding = false, + ) + .background(TangemTheme.colors.background.action) + .clickable { onOptionClicked(item) }, + showDivider = index != allMyNetworks.lastIndex, + ) { + InputRowChecked( + text = item.text, + checked = item.isSelected, + ) + } + } +} + +private fun LazyListScope.networksHeader() { + item(key = "networks_header") { + SpacerH(TangemTheme.dimens.spacing16) + + Text( + modifier = Modifier + .fillMaxWidth() + .background( + color = TangemTheme.colors.background.action, + shape = RoundedCornerShape( + topStart = 16.dp, + topEnd = 16.dp, + ), + ) + .padding( + start = 12.dp, + end = 12.dp, + top = 12.dp, + bottom = 4.dp, + ), + text = stringResourceSafe(id = R.string.earn_filter_networks), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +private fun LazyListScope.specificNetworksList( + specificNetworks: List, + onOptionClicked: (EarnFilterNetworkUM) -> Unit, +) { + itemsIndexed( + items = specificNetworks, + key = { _, item -> item.id }, + ) { index, item -> + DividerContainer( + modifier = Modifier + .height(52.dp) + .roundedShapeItemDecoration( + currentIndex = index + 1, + lastIndex = specificNetworks.lastIndex + 1, + addDefaultPadding = false, + ) + .background(TangemTheme.colors.background.action) + .clickable { onOptionClicked(item) }, + showDivider = false, + ) { + RowContentContainer( + modifier = Modifier + .heightIn(52.dp) + .padding(horizontal = 12.dp), + icon = { + CoinIcon( + modifier = Modifier.size(22.dp), + url = item.iconUrl, + alpha = 1f, + colorFilter = null, + fallbackResId = R.drawable.ic_custom_token_44, + ) + }, + text = { + RowText( + mainText = item.text.resolveReference(), + secondText = item.symbol.resolveReference(), + accentMainText = true, + accentSecondText = false, + ) + }, + action = { + if (item.isSelected) { + Icon( + painter = painterResource(R.drawable.ic_check_24), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + ) + } + }, + ) + } + } +} + +@Preview(widthDp = 360, heightDp = 800) +@Preview(widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview( + alwaysShowBottomSheets = true, + ) { + Box(Modifier.background(TangemTheme.colors.background.secondary)) { + EarnFilterByNetworkBottomSheet( + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = EarnFilterByNetworkBottomSheetContentUM( + selectedNetwork = EarnFilterNetworkUM.AllNetworks( + text = resourceReference(R.string.earn_filter_all_networks), + isSelected = true, + ), + networks = persistentListOf( + EarnFilterNetworkUM.AllNetworks( + text = resourceReference(R.string.earn_filter_all_networks), + isSelected = true, + ), + EarnFilterNetworkUM.MyNetworks( + text = stringReference("My network"), + isSelected = false, + ), + EarnFilterNetworkUM.Network( + id = "ethereum", + text = stringReference("Ethereum"), + symbol = stringReference("ETH"), + iconUrl = null, + isSelected = false, + ), + EarnFilterNetworkUM.Network( + id = "polygon", + text = stringReference("Polygon"), + symbol = stringReference("MATIC"), + iconUrl = null, + isSelected = false, + ), + ), + onOptionClick = {}, + ), + ), + ) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByTypeBottomSheet.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByTypeBottomSheet.kt new file mode 100644 index 0000000000..7790ff977e --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByTypeBottomSheet.kt @@ -0,0 +1,85 @@ +package com.tangem.features.feed.ui.earn.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.components.inputrow.InputRowChecked +import com.tangem.core.ui.components.inputrow.inner.DividerContainer +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.feed.impl.R +import com.tangem.features.feed.ui.earn.state.EarnFilterByTypeBottomSheetContentUM +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM + +@Composable +internal fun EarnFilterByTypeBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + titleText = resourceReference(R.string.earn_filter_by), + containerColor = TangemTheme.colors.background.tertiary, + content = { Content(it) }, + ) +} + +@Composable +private fun Content(content: EarnFilterByTypeBottomSheetContentUM) { + Column( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + ), + ) { + EarnFilterTypeUM.entries.forEachIndexed { index, type -> + DividerContainer( + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = EarnFilterTypeUM.entries.lastIndex, + addDefaultPadding = false, + ) + .background(TangemTheme.colors.background.action) + .clickable { content.onOptionClick(type) }, + showDivider = index != EarnFilterTypeUM.entries.lastIndex, + ) { + InputRowChecked( + text = type.text, + checked = type == content.selectedOption, + ) + } + } + } +} + +@Preview(widthDp = 360, heightDp = 640) +@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview( + alwaysShowBottomSheets = true, + ) { + Box(Modifier.background(TangemTheme.colors.background.secondary)) { + EarnFilterByTypeBottomSheet( + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = {}, + content = EarnFilterByTypeBottomSheetContentUM( + selectedOption = EarnFilterTypeUM.All, + onOptionClick = {}, + ), + ), + ) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByNetworkBottomSheetContentUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByNetworkBottomSheetContentUM.kt new file mode 100644 index 0000000000..101b80fbce --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByNetworkBottomSheetContentUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.feed.ui.earn.state + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import kotlinx.collections.immutable.ImmutableList + +internal data class EarnFilterByNetworkBottomSheetContentUM( + val selectedNetwork: EarnFilterNetworkUM, + val networks: ImmutableList, + val onOptionClick: (EarnFilterNetworkUM) -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByTypeBottomSheetContentUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByTypeBottomSheetContentUM.kt new file mode 100644 index 0000000000..25de36afa0 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByTypeBottomSheetContentUM.kt @@ -0,0 +1,8 @@ +package com.tangem.features.feed.ui.earn.state + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +internal data class EarnFilterByTypeBottomSheetContentUM( + val selectedOption: EarnFilterTypeUM, + val onOptionClick: (EarnFilterTypeUM) -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt new file mode 100644 index 0000000000..1fd414edf4 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt @@ -0,0 +1,29 @@ +package com.tangem.features.feed.ui.earn.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference + +@Immutable +internal sealed class EarnFilterNetworkUM { + + abstract val text: TextReference + abstract val isSelected: Boolean + + data class AllNetworks( + override val text: TextReference, + override val isSelected: Boolean, + ) : EarnFilterNetworkUM() + + data class MyNetworks( + override val text: TextReference, + override val isSelected: Boolean, + ) : EarnFilterNetworkUM() + + data class Network( + val id: String, + override val text: TextReference, + val symbol: TextReference, + val iconUrl: String?, + override val isSelected: Boolean, + ) : EarnFilterNetworkUM() +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterTypeUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterTypeUM.kt new file mode 100644 index 0000000000..0738da510c --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterTypeUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.feed.ui.earn.state + +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference + +internal enum class EarnFilterTypeUM(val text: TextReference) { + All(resourceReference(R.string.earn_filter_all_types)), + Staking(resourceReference(R.string.common_staking)), + YieldMode(resourceReference(R.string.markets_sort_by_yield_mode_title)), +} \ No newline at end of file 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 ff28635572..358cab5ee9 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 @@ -12,6 +12,15 @@ internal sealed interface EarnListUM { data class Error(val onRetryClicked: () -> Unit) : EarnListUM } +@Immutable +internal sealed interface EarnBestOpportunitiesUM { + data object Loading : EarnBestOpportunitiesUM + data object Empty : EarnBestOpportunitiesUM + data class EmptyFiltered(val onClearFilterClick: () -> Unit) : EarnBestOpportunitiesUM + data class Content(val items: ImmutableList) : EarnBestOpportunitiesUM + data class Error(val onRetryClicked: () -> Unit) : EarnBestOpportunitiesUM +} + @Immutable internal data class EarnListItemUM( val network: TextReference, 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 8425666307..a0b6870d0a 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,26 +1,25 @@ package com.tangem.features.feed.ui.earn.state import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference -import kotlinx.collections.immutable.ImmutableList @Immutable internal data class EarnUM( val mostlyUsed: EarnListUM, - val bestOpportunities: EarnListUM, - val selectedNetworkFilter: EarnFilterUM?, - val selectedTypeFilter: EarnFilterUM?, - val networkFilters: ImmutableList, - val typeFilters: ImmutableList, + val bestOpportunities: EarnBestOpportunitiesUM, + val selectedTypeFilter: EarnFilterTypeUM, + val selectedNetworkFilter: EarnFilterNetworkUM, + val filterByTypeBottomSheet: TangemBottomSheetConfig, + val filterByNetworkBottomSheet: TangemBottomSheetConfig, val onBackClick: () -> Unit, val onNetworkFilterClick: () -> Unit, val onTypeFilterClick: () -> Unit, -) +) { -@Immutable -internal data class EarnFilterUM( - val id: String, - val name: TextReference, - val isSelected: Boolean, - val onClick: () -> Unit, -) \ No newline at end of file + val selectedNetworkFilterText: TextReference + get() = selectedNetworkFilter.text + + val selectedTypeFilterText: TextReference + get() = selectedTypeFilter.text +} \ No newline at end of file From 1abd6b9b2e6185af13aaa2e60cff3d6ac16e7b0a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Feb 2026 10:46:19 +0300 Subject: [PATCH 020/106] Updated on 2026-08-14 --- .../model/intents/WalletWarningsClickIntents.kt | 11 +++++++++++ .../wallet/domain/GetMultiWalletWarningsFactory.kt | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) 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 cbadd05712..0dd7d1af6f 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 @@ -79,6 +79,8 @@ internal interface WalletWarningsClickIntents { fun onSupportClick() + fun onBackupErrorClick() + fun onNoteMigrationButtonClick(url: String) fun onSeedPhraseNotificationConfirm() @@ -332,6 +334,15 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } + override fun onBackupErrorClick() { + val userWallet = getSelectedUserWallet() ?: return + + modelScope.launch { + val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch + sendFeedbackEmailUseCase(type = FeedbackEmailType.BackupProblem(walletMetaInfo = metaInfo)) + } + } + override fun onNoteMigrationButtonClick(url: String) { analyticsEventHandler.send(MainScreen.NotePromoButton()) modelScope.launch(dispatchers.main) { 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 fe3861e0c2..6381c3f6a4 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 @@ -182,7 +182,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val cardTypesResolver = userWallet.scanResponse.cardTypesResolver addIf( - element = WalletNotification.Critical.BackupError { clickIntents.onSupportClick() }, + element = WalletNotification.Critical.BackupError { clickIntents.onBackupErrorClick() }, condition = !backupValidator.isValidBackupStatus(userWallet.scanResponse.card) || userWallet.hasBackupError, ) From 9d73e02d69535d9c432a508829048613b744f375 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Feb 2026 11:54:03 +0300 Subject: [PATCH 021/106] Updated on 2026-08-14 --- .../kotlin/com/tangem/common/BaseTestCase.kt | 2 + .../tangem/common/extensions/UiDeviceExt.kt | 12 +- .../com/tangem/scenarios/MarketsScenarios.kt | 26 ++ .../com/tangem/scenarios/SwapScenarios.kt | 180 +++++++++ .../tangem/screens/SwapStoriesPageObject.kt | 21 + .../com/tangem/tests/swap/SwapStoriesTest.kt | 378 ++++++++++++++++-- .../ui/swapStoriesScreen/SwapStoriesScreen.kt | 2 + .../stories/inner/StoriesProgressBar.kt | 7 +- .../core/ui/test/SwapStoriesScreenTestTags.kt | 3 + 9 files changed, 588 insertions(+), 43 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/scenarios/MarketsScenarios.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index df3927d4fb..a7aa7666e3 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -84,6 +84,7 @@ abstract class BaseTestCase : TestCase( * – and only after that the activity should be launched. */ protected fun setupHooks( + additionalBeforeAppLaunchSection: () -> Unit = {}, additionalBeforeSection: () -> Unit = {}, additionalAfterSection: () -> Unit = {}, ) = before { @@ -91,6 +92,7 @@ abstract class BaseTestCase : TestCase( // Setup WireMock redirect for CI with local WireMock instances val wiremockUrl = InstrumentationRegistry.getArguments().getString(WIREMOCK_BASE_URL_ARG) WireMockRedirectInterceptor.overriddenBaseUrl = wiremockUrl + additionalBeforeAppLaunchSection() hiltRule.inject() runBlocking { appPreferencesStore.editData { mutablePreferences -> diff --git a/app/src/androidTest/kotlin/com/tangem/common/extensions/UiDeviceExt.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/UiDeviceExt.kt index 992a417013..03dcf422f5 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/extensions/UiDeviceExt.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/UiDeviceExt.kt @@ -2,6 +2,7 @@ package com.tangem.common.extensions import androidx.test.uiautomator.By import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG import com.tangem.wallet.R import io.github.kakaocup.kakao.common.utilities.getResourceString @@ -74,8 +75,17 @@ fun BaseTestCase.stopApp(packageName: String) { } fun BaseTestCase.launchApp(packageName: String) { - device.apps.waitForAppLaunchAndReady(packageName = packageName) device.apps.launch(packageName) + device.apps.waitForAppLaunchAndReady(packageName = packageName) +} + +fun BaseTestCase.restartApp(packageName: String) { + device.uiDevice.pressHome() + device.apps.kill(packageName) + waitForIdle() + device.apps.launch(packageName) + device.apps.waitForAppLaunchAndReady(timeout = WAIT_UNTIL_TIMEOUT_LONG, packageName = packageName) + waitForIdle() } enum class SwipeDirection { diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/MarketsScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/MarketsScenarios.kt new file mode 100644 index 0000000000..02142e077c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/MarketsScenarios.kt @@ -0,0 +1,26 @@ +package com.tangem.scenarios + +import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.screens.onMainScreen +import com.tangem.screens.onMarketsScreen +import com.tangem.screens.onMarketsTokenDetailsScreen +import io.qameta.allure.kotlin.Allure.step + +fun BaseTestCase.openMarketTokenDetailsScreen(blockchainName: String, tokenName: String) { + step("Open 'Markets' screen") { + onMainScreen { searchThroughMarketPlaceholder.performClick() } + waitForIdle() + } + step("Click on 'Search' placeholder") { + onMarketsScreen { searchThroughMarketPlaceholder.performClick() } + } + step("Click on $blockchainName blockchain") { + waitForIdle() + onMarketsScreen { tokenWithTitle(blockchainName).clickWithAssertion() } + } + step("Click on $tokenName token") { + waitForIdle() + onMarketsTokenDetailsScreen { tokenWithTitle(tokenName).clickWithAssertion() } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt new file mode 100644 index 0000000000..a86ca56854 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt @@ -0,0 +1,180 @@ +package com.tangem.scenarios + +import androidx.compose.ui.test.click +import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.screens.* +import io.github.kakaocup.kakao.common.utilities.getResourceString +import io.qameta.allure.kotlin.Allure.step +import com.tangem.common.ui.R as CommonUiR + +private val firstStoryIndex = 0 +private val firstStoryTitle = getResourceString(CommonUiR.string.swap_story_first_title) +private val firstStorySubtitle = getResourceString(CommonUiR.string.swap_story_first_subtitle) +private val secondStoryIndex = 1 +private val secondStoryTitle = getResourceString(CommonUiR.string.swap_story_second_title) +private val secondStorySubtitle = getResourceString(CommonUiR.string.swap_story_second_subtitle) +private val thirdStoryIndex = 2 +private val thirdStoryTitle = getResourceString(CommonUiR.string.swap_story_third_title) +private val thirdStorySubtitle = getResourceString(CommonUiR.string.swap_story_third_subtitle) +private val forthStoryIndex = 3 +private val forthStoryTitle = getResourceString(CommonUiR.string.swap_story_forth_title) +private val forthStorySubtitle = getResourceString(CommonUiR.string.swap_story_forth_subtitle) +private val fifthStoryIndex = 4 +private val fifthStoryTitle = getResourceString(CommonUiR.string.swap_story_fifth_title) +private val fifthStorySubtitle = getResourceString(CommonUiR.string.swap_story_fifth_subtitle) + +fun BaseTestCase.openSwapScreen( + from: SwapEntryPoint, + storiesExist: Boolean = true, +) { + when (from) { + SwapEntryPoint.MainScreen -> step("Click on 'Swap' button on 'Main' screen") { + onMainScreen { swapButton.performClick() } + } + + SwapEntryPoint.TokenDetails -> step("Click on 'Swap' button on 'Token details' screen") { + onTokenDetailsScreen { swapButton().performClick() } + } + + SwapEntryPoint.MarketsTokenDetails -> step("Click on 'Swap' button on 'Markets' token details screen") { + onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.performClick() } + } + + SwapEntryPoint.TokenActionsBottomSheet -> step("Click on 'Swap' button on token actions bottom sheet") { + onTokenActionsBottomSheet { swapButton.performClick() } + } + } + + if (storiesExist) { + step("Close 'Stories' screen") { + onSwapStoriesScreen { closeButton.clickWithAssertion() } + } + } else { + step("Assert 'Stories' screen is not displayed") { + onSwapStoriesScreen { container.assertDoesNotExist() } + } + } + + step("Assert 'Swap' screen title is displayed") { + onSwapTokenScreen { title.assertIsDisplayed() } + } +} + +fun BaseTestCase.checkStoriesContent( + storyIndex: Int, + storyTitle: String, + storySubtitle: String, + ) { + step("Assert 'Close' button is displayed") { + onSwapStoriesScreen { closeButton.assertIsDisplayed() } + } + step("Assert progress bar item №${storyIndex + 1} is displayed") { + onSwapStoriesScreen { progressBarItem(storyIndex).assertIsDisplayed() } + } + step("Assert story title is $storyTitle") { + onSwapStoriesScreen { title.assertTextContains(storyTitle) } + } + step("Assert story subtitle is $storySubtitle") { + onSwapStoriesScreen { subtitle.assertTextContains(storySubtitle) } + } +} + +fun BaseTestCase.checkStoriesChanges() { + step("Check title and subtitle for story №${firstStoryIndex + 1}") { + checkStoriesContent( + storyIndex = firstStoryIndex, + storyTitle = firstStoryTitle, + storySubtitle = firstStorySubtitle + ) + } + step("Click on right side") { + onSwapStoriesScreen { container.performTouchInput { click(centerRight) } } + } + step("Check title and subtitle for story №${secondStoryIndex + 1}") { + checkStoriesContent( + storyIndex = secondStoryIndex, + storyTitle = secondStoryTitle, + storySubtitle = secondStorySubtitle + ) + } + step("Click on right side") { + onSwapStoriesScreen { container.performTouchInput { click(centerRight) } } + } + step("Check title and subtitle for story №${thirdStoryIndex + 1}") { + checkStoriesContent( + storyIndex = thirdStoryIndex, + storyTitle = thirdStoryTitle, + storySubtitle = thirdStorySubtitle + ) + } + step("Click on right side") { + onSwapStoriesScreen { container.performTouchInput { click(centerRight) } } + } + step("Check title and subtitle for story №${forthStoryIndex + 1}") { + checkStoriesContent( + storyIndex = forthStoryIndex, + storyTitle = forthStoryTitle, + storySubtitle = forthStorySubtitle + ) + } + step("Click on right side") { + onSwapStoriesScreen { container.performTouchInput { click(centerRight) } } + } + step("Check title and subtitle for story №${fifthStoryIndex + 1}") { + checkStoriesContent( + storyIndex = fifthStoryIndex, + storyTitle = fifthStoryTitle, + storySubtitle = fifthStorySubtitle + ) + } + step("Click on left side") { + onSwapStoriesScreen { container.performTouchInput { click(centerLeft) } } + } + step("Check title and subtitle for story №${forthStoryIndex + 1}") { + checkStoriesContent( + storyIndex = forthStoryIndex, + storyTitle = forthStoryTitle, + storySubtitle = forthStorySubtitle + ) + } + step("Click on left side") { + onSwapStoriesScreen { container.performTouchInput { click(centerLeft) } } + } + step("Check title and subtitle for story №${thirdStoryIndex + 1}") { + checkStoriesContent( + storyIndex = thirdStoryIndex, + storyTitle = thirdStoryTitle, + storySubtitle = thirdStorySubtitle + ) + } + step("Click on left side") { + onSwapStoriesScreen { container.performTouchInput { click(centerLeft) } } + } + step("Check title and subtitle for story №${secondStoryIndex + 1}") { + checkStoriesContent( + storyIndex = secondStoryIndex, + storyTitle = secondStoryTitle, + storySubtitle = secondStorySubtitle + ) + } + step("Click on left side") { + onSwapStoriesScreen { container.performTouchInput { click(centerLeft) } } + } + step("Check title and subtitle for story №${firstStoryIndex + 1}") { + checkStoriesContent( + storyIndex = firstStoryIndex, + storyTitle = firstStoryTitle, + storySubtitle = firstStorySubtitle + ) + } +} + +sealed class SwapEntryPoint { + object MainScreen : SwapEntryPoint() + object TokenDetails : SwapEntryPoint() + object MarketsTokenDetails : SwapEntryPoint() + object TokenActionsBottomSheet : SwapEntryPoint() +} + + diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapStoriesPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapStoriesPageObject.kt index e48a981112..f9d696a638 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapStoriesPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapStoriesPageObject.kt @@ -10,10 +10,31 @@ import io.github.kakaocup.compose.node.element.KNode class SwapStoriesPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { + val container: KNode = child { + hasTestTag(SwapStoriesScreenTestTags.SCREEN_CONTAINER) + useUnmergedTree = true + } + val closeButton: KNode = child { hasTestTag(SwapStoriesScreenTestTags.CLOSE_BUTTON) useUnmergedTree = true } + + fun progressBarItem(index: Int): KNode = child { + hasTestTag(SwapStoriesScreenTestTags.PROGRESS_BAR_ITEM) + hasPosition(index) + useUnmergedTree = true + } + + val title: KNode = child { + hasTestTag(SwapStoriesScreenTestTags.TITLE) + useUnmergedTree = true + } + + val subtitle: KNode = child { + hasTestTag(SwapStoriesScreenTestTags.SUBTITLE) + useUnmergedTree = true + } } internal fun BaseTestCase.onSwapStoriesScreen(function: SwapStoriesPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt index ccbca2b78c..2f3f1a81d6 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt @@ -1,10 +1,13 @@ package com.tangem.tests.swap +import androidx.compose.ui.test.longClick +import androidx.test.InstrumentationRegistry.getTargetContext import com.tangem.common.BaseTestCase import com.tangem.common.extensions.assertHasBadge -import com.tangem.common.extensions.clickWithAssertion -import com.tangem.scenarios.openMainScreen -import com.tangem.scenarios.synchronizeAddresses +import com.tangem.common.extensions.restartApp +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.scenarios.* import com.tangem.screens.* import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId @@ -30,14 +33,8 @@ class SwapStoriesTest : BaseTestCase() { step("Assert 'Swap' button has badge") { onMainScreen { swapButton.assertHasBadge() } } - step("Click on 'Swap' button") { - onMainScreen { swapButton.performClick() } - } - step("Close 'Stories' screen") { - onSwapStoriesScreen { closeButton.clickWithAssertion() } - } - step("Assert 'Swap' screen title is displayed") { - onSwapTokenScreen { title.assertIsDisplayed() } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.MainScreen) } step("Click on 'Close' button") { onSwapTokenScreen { closeButton.performClick() } @@ -68,14 +65,8 @@ class SwapStoriesTest : BaseTestCase() { step("Assert 'Swap' button has badge") { onTokenDetailsScreen { swapButton().assertHasBadge() } } - step("Click on 'Swap' button") { - onTokenDetailsScreen { swapButton().performClick() } - } - step("Close 'Stories' screen") { - onSwapStoriesScreen { closeButton.clickWithAssertion() } - } - step("Assert 'Swap' screen title is displayed") { - onSwapTokenScreen { title.assertIsDisplayed() } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) } step("Click on 'Close' button") { onSwapTokenScreen { closeButton.performClick() } @@ -102,33 +93,15 @@ class SwapStoriesTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses() } - step("Open 'Markets' screen") { - onMainScreen { searchThroughMarketPlaceholder.performClick() } - waitForIdle() - } - step("") { - onMarketsScreen { searchThroughMarketPlaceholder.performClick() } - } - step("Click on $tokenName token") { - waitForIdle() - onMarketsScreen { tokenWithTitle(tokenName).clickWithAssertion() } - } - step("Click on $tokenName token") { - waitForIdle() - onMarketsTokenDetailsScreen { tokenWithTitle(tokenName).performClick() } + step("Open 'Markets' token details screen for token '$tokenName'") { + openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName) } step("Assert 'Swap' button has badge") { onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertIsDisplayed() } onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeShown) } } - step("Click on 'Swap' button") { - onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.performClick() } - } - step("Close 'Stories' screen") { - onSwapStoriesScreen { closeButton.clickWithAssertion() } - } - step("Assert 'Swap' screen title is displayed") { - onSwapTokenScreen { title.assertIsDisplayed() } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails) } step("Click on 'Close' button") { onSwapTokenScreen { closeButton.performClick() } @@ -138,4 +111,327 @@ class SwapStoriesTest : BaseTestCase() { } } } + + @AllureId("5469") + @DisplayName("Check unavailable swap stories on 'Main' screen") + @Test + fun checkUnavailableSwapStoriesOnMainScreen() { + val scenarioName = "stories_first_time_swap" + val scenarioErrorState = "Error" + val packageName = getTargetContext().packageName + + setupHooks( + additionalBeforeAppLaunchSection = { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioErrorState) + }, + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Assert 'Swap' button has badge") { + onMainScreen { swapButton.assertHasBadge(false) } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.MainScreen, storiesExist = false) + } + step("Reset WireMock scenario state") { + resetWireMockScenarioState(scenarioName) + } + step("Click on 'Close' button") { + onSwapTokenScreen { closeButton.performClick() } + } + step("Restart app") { + restartApp(packageName) + } + step("Assert 'Swap' button is displayed") { + waitForIdle() + onMainScreen { swapButton.assertIsDisplayed() } + } + step("Assert 'Swap' button has badge") { + onMainScreen { swapButton.assertHasBadge() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.MainScreen, storiesExist = true) + } + } + } + + @AllureId("5471") + @DisplayName("Check unavailable swap stories on 'Token details' screen") + @Test + fun checkUnavailableSwapStoriesOnTokenDetailsScreen() { + val scenarioName = "stories_first_time_swap" + val scenarioErrorState = "Error" + val packageName = getTargetContext().packageName + val tokenName = "Ethereum" + + + setupHooks( + additionalBeforeAppLaunchSection = { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioErrorState) + }, + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() } + } + step("Assert 'Swap' button has badge") { + onTokenDetailsScreen { swapButton().assertHasBadge(false) } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false) + } + step("Click on 'Close' button") { + onSwapTokenScreen { closeButton.performClick() } + } + step("Reset WireMock scenario state") { + resetWireMockScenarioState(scenarioName) + } + step("Restart app") { + restartApp(packageName) + } + step("Assert 'Swap' button has badge") { + waitForIdle() + onMainScreen { swapButton.assertHasBadge() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = true) + } + } + } + + @AllureId("5471") + @DisplayName("Check unavailable swap stories on 'Markets' token details screen") + @Test + fun checkUnavailableSwapStoriesOnMarketsTokenDetailsScreen() { + val scenarioName = "stories_first_time_swap" + val scenarioErrorState = "Error" + val packageName = getTargetContext().packageName + val tokenName = "Ethereum" + val badgeShown = "Badge shown" + val badgeHidden = "Badge hidden" + + setupHooks( + additionalBeforeAppLaunchSection = { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioErrorState) + }, + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Markets' token details screen for token '$tokenName'") { + openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName) + } + step("Assert 'Swap' button has badge") { + waitForIdle() + onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertIsDisplayed() } + onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeHidden) } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails, storiesExist = false) + } + step("Click on 'Close' button") { + onSwapTokenScreen { closeButton.performClick() } + } + step("Reset WireMock scenario state") { + resetWireMockScenarioState(scenarioName) + } + step("Restart app") { + restartApp(packageName) + } + step("Open 'Markets' token details screen for token '$tokenName'") { + openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName) + } + step("Assert 'Swap' button has badge") { + waitForIdle() + onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertIsDisplayed() } + onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeShown) } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails, storiesExist = true) + } + } + } + + @AllureId("5474") + @DisplayName("Check 'Swap' stories on 'Main' screen") + @Test + fun checkSwapStoriesOnMainScreenTest() { + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on 'Swap' button on 'Main' screen") { + onMainScreen { swapButton.performClick() } + } + step("Check stories changes") { + checkStoriesChanges() + } + step("Click on 'Close' button") { + onSwapStoriesScreen { closeButton.performClick() } + } + step("Assert 'Swap' screen title is displayed") { + onSwapTokenScreen { title.assertIsDisplayed() } + } + step("Click on 'Close' button") { + onSwapTokenScreen { closeButton.performClick() } + } + step("Open 'Swap' screen without stories") { + openSwapScreen(from = SwapEntryPoint.MainScreen, storiesExist = false) + } + } + } + + @AllureId("5475") + @DisplayName("Check 'Swap' stories on 'Token details' screen") + @Test + fun checkSwapStoriesOnTokenDetailsScreenTest() { + val tokenName = "Ethereum" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() } + } + step("Assert 'Swap' button has badge") { + onTokenDetailsScreen { swapButton().assertHasBadge() } + } + step("Click on 'Swap' button on 'Token details' screen") { + onTokenDetailsScreen { swapButton().performClick() } + } + step("Check stories changes") { + checkStoriesChanges() + } + step("Click on 'Close' button") { + onSwapStoriesScreen { closeButton.performClick() } + } + step("Assert 'Swap' screen title is displayed") { + onSwapTokenScreen { title.assertIsDisplayed() } + } + step("Click on 'Close' button") { + onSwapTokenScreen { closeButton.performClick() } + } + step("Open 'Swap' screen without stories") { + openSwapScreen(from = SwapEntryPoint.TokenDetails, storiesExist = false) + } + } + } + + @AllureId("5476") + @DisplayName("Check 'Swap' stories on 'Markets' screen") + @Test + fun checkSwapStoriesOnMarketTokenDetailsScreenTest() { + val tokenName = "Ethereum" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Open 'Markets' token details screen for token '$tokenName'") { + openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName) + } + step("Click on 'Swap' button on 'Markets' token details screen") { + onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.performClick() } + } + step("Check stories changes") { + checkStoriesChanges() + } + step("Click on 'Close' button") { + onSwapStoriesScreen { closeButton.performClick() } + } + step("Assert 'Swap' screen title is displayed") { + onSwapTokenScreen { title.assertIsDisplayed() } + } + step("Click on 'Close' button") { + onSwapTokenScreen { closeButton.performClick() } + } + step("Open 'Swap' screen without stories") { + openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails, storiesExist = false) + } + } + } + + @AllureId("5477") + @DisplayName("Check 'Swap' stories on token actions bottom sheet") + @Test + fun checkSwapStoriesOnTokenActionsBottomSheetTest() { + val tokenName = "Ethereum" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Long click on token with name: '$tokenName'") { + waitForIdle() + onMainScreen { + tokenWithTitleAndAddress(tokenName).performTouchInput { + longClick( + position = center, + durationMillis = 1000L + ) + } + } + } + step("Click on 'Swap' button") { + onTokenActionsBottomSheet { swapButton.performClick() } + } + step("Check stories changes") { + checkStoriesChanges() + } + step("Click on 'Close' button") { + onSwapStoriesScreen { closeButton.performClick() } + } + step("Assert 'Swap' screen title is displayed") { + onSwapTokenScreen { title.assertIsDisplayed() } + } + step("Click on 'Close' button") { + onSwapTokenScreen { closeButton.performClick() } + } + step("Open 'Swap' screen without stories") { + openSwapScreen(from = SwapEntryPoint.TokenActionsBottomSheet, storiesExist = false) + } + } + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/swapStoriesScreen/SwapStoriesScreen.kt b/common/ui/src/main/java/com/tangem/common/ui/swapStoriesScreen/SwapStoriesScreen.kt index dede83288b..cd75f1b70f 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/swapStoriesScreen/SwapStoriesScreen.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/swapStoriesScreen/SwapStoriesScreen.kt @@ -94,6 +94,7 @@ private fun SwapStoriesText(current: SwapStoriesUM.Content.Config) { ), color = TangemTheme.colors.text.constantWhite, textAlign = TextAlign.Center, + modifier = Modifier.testTag(SwapStoriesScreenTestTags.TITLE), ) Text( text = current.subtitle.resolveReference(), @@ -105,6 +106,7 @@ private fun SwapStoriesText(current: SwapStoriesUM.Content.Config) { ), color = SubtitleColor, textAlign = TextAlign.Center, + modifier = Modifier.testTag(SwapStoriesScreenTestTags.SUBTITLE), ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesProgressBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesProgressBar.kt index 05d61b51f0..00592f5bf2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesProgressBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/stories/inner/StoriesProgressBar.kt @@ -8,16 +8,20 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext +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.SpacerW4 import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.SwapStoriesScreenTestTags import kotlinx.coroutines.delay private const val STORIES_ANIMATION_SPEED_ZERO_DURATION = 3000L @@ -99,6 +103,7 @@ fun StoriesProgressBar( .clip(RoundedCornerShape(2.dp)) .background(TangemColorPalette.White) .fillMaxHeight() + .testTag(SwapStoriesScreenTestTags.PROGRESS_BAR_ITEM) .let { modifier -> when (index) { currentStep -> modifier.fillMaxWidth(progress.value) diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SwapStoriesScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SwapStoriesScreenTestTags.kt index 40a0ffe621..b61b3e88c4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/SwapStoriesScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwapStoriesScreenTestTags.kt @@ -3,4 +3,7 @@ package com.tangem.core.ui.test object SwapStoriesScreenTestTags { const val SCREEN_CONTAINER = "SWAP_STORIES_SCREEN_CONTAINER" const val CLOSE_BUTTON = "SWAP_STORIES_SCREEN_CLOSE_BUTTON" + const val PROGRESS_BAR_ITEM = "SWAP_STORIES_SCREEN_PROGRESS_BAR_ITEM" + const val TITLE = "SWAP_STORIES_SCREEN_TITLE" + const val SUBTITLE = "SWAP_STORIES_SCREEN_SUBTITLE" } \ No newline at end of file From f9226ab6bf8fb017a65febaee1a60cb84e866fd5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Feb 2026 13:54:38 +0500 Subject: [PATCH 022/106] Updated on 2026-08-14 --- common/build.gradle.kts | 4 ++ .../tangem/common/CryptoCurrencyStatusExt.kt | 33 ++++++++++++++++ .../com/tangem/common}/StakingBalanceExt.kt | 2 +- common/ui/build.gradle.kts | 1 + .../ui/tokens/TokenItemStateConverter.kt | 39 ++++++------------- data/tokens/build.gradle.kts | 1 + .../DefaultCurrencyChecksRepository.kt | 2 +- domain/tokens/build.gradle.kts | 1 + .../tokens/operations/TokenListFactory.kt | 16 ++------ .../operations/TotalFiatBalanceCalculator.kt | 2 +- .../OnrampTokenItemStateConverterFactory.kt | 18 +++++++-- .../referral/model/AccountAwardConverter.kt | 18 +++++++-- .../impl/presentation/model/StakingModel.kt | 2 +- .../converters/AccountTokenItemConverter.kt | 18 +++++++-- ...TokenDetailsBalanceSelectStateConverter.kt | 2 +- .../TokenDetailsLoadedBalanceConverter.kt | 2 +- .../TokenDetailsStakingInfoConverter.kt | 4 +- .../CryptoCurrencyToDraggableItemConverter.kt | 2 +- ...ryptoCurrencyToDraggableItemConverterV2.kt | 2 +- 19 files changed, 106 insertions(+), 63 deletions(-) create mode 100644 common/src/main/kotlin/com/tangem/common/CryptoCurrencyStatusExt.kt rename {domain/staking/src/main/java/com/tangem/domain/staking/utils => common/src/main/kotlin/com/tangem/common}/StakingBalanceExt.kt (98%) diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 055efc6a2a..30cd9f7258 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -13,6 +13,10 @@ android { dependencies { implementation(projects.core.utils) + api(projects.domain.models) + api(projects.domain.appCurrency.models) + api(projects.domain.staking.models) + api(projects.libs.crypto) // region Firebase libraries implementation(platform(deps.firebase.bom)) diff --git a/common/src/main/kotlin/com/tangem/common/CryptoCurrencyStatusExt.kt b/common/src/main/kotlin/com/tangem/common/CryptoCurrencyStatusExt.kt new file mode 100644 index 0000000000..038cdc2eba --- /dev/null +++ b/common/src/main/kotlin/com/tangem/common/CryptoCurrencyStatusExt.kt @@ -0,0 +1,33 @@ +package com.tangem.common + +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.staking.StakingBalance +import com.tangem.utils.extensions.orZero +import java.math.BigDecimal + +/** + * Calculates the total fiat amount by adding the main fiat amount and the fiat value of the staked balance. + */ +fun CryptoCurrencyStatus.getTotalFiatAmount(): BigDecimal? { + val fiatAmount = value.fiatAmount + + val fiatStakedBalance = value.fiatRate?.times(getStakedBalance().orZero()) ?: return fiatAmount + val totalAmount = fiatAmount?.plus(fiatStakedBalance) ?: return fiatStakedBalance + + return totalAmount +} + +/** + * Calculates the total cryptocurrency amount by adding the main crypto amount and the staked balance. + */ +fun CryptoCurrencyStatus.getTotalCryptoAmount(): BigDecimal? { + val cryptoAmount = value.amount + + val cryptoStakedBalance = getStakedBalance() ?: return cryptoAmount + val totalAmount = cryptoAmount?.plus(cryptoStakedBalance) ?: return cryptoStakedBalance + + return totalAmount +} + +private fun CryptoCurrencyStatus.getStakedBalance() = (value.stakingBalance as? StakingBalance.Data) + ?.getTotalWithRewardsStakingBalance(blockchainId = currency.network.rawId) \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/utils/StakingBalanceExt.kt b/common/src/main/kotlin/com/tangem/common/StakingBalanceExt.kt similarity index 98% rename from domain/staking/src/main/java/com/tangem/domain/staking/utils/StakingBalanceExt.kt rename to common/src/main/kotlin/com/tangem/common/StakingBalanceExt.kt index 66c7a151db..9994416d24 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/utils/StakingBalanceExt.kt +++ b/common/src/main/kotlin/com/tangem/common/StakingBalanceExt.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.staking.utils +package com.tangem.common import com.tangem.domain.models.staking.BalanceType import com.tangem.domain.models.staking.StakingBalance diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index 9583112c85..1714d0affb 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -9,6 +9,7 @@ android { } dependencies { + api(projects.common) /** Compose */ implementation(deps.compose.material3) diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index 6211063758..4ffc157fb0 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -1,5 +1,7 @@ package com.tangem.common.ui.tokens +import com.tangem.common.getTotalCryptoAmount +import com.tangem.common.getTotalFiatAmount import com.tangem.common.ui.R import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter @@ -25,11 +27,8 @@ import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingOption import com.tangem.domain.staking.model.common.RewardInfo import com.tangem.domain.staking.model.common.RewardType -import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal @@ -156,29 +155,6 @@ class TokenItemStateConverter( } companion object { - - fun CryptoCurrencyStatus.getFormattedFiatAmount(appCurrency: AppCurrency): String { - val fiatAmount = value.fiatAmount ?: return DASH_SIGN - - val fiatYieldBalance = value.fiatRate?.times(getStakedBalance()).orZero() - val totalAmount = fiatAmount.plus(fiatYieldBalance) - - return totalAmount.format { - fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) - } - } - - fun CryptoCurrencyStatus.getFormattedCryptoAmount(): String { - val cryptoAmount = value.amount ?: return DASH_SIGN - - val totalAmount = cryptoAmount.plus(getStakedBalance()) - - return totalAmount.format { crypto(currency) } - } - - private fun CryptoCurrencyStatus.getStakedBalance() = (value.stakingBalance as? StakingBalance.Data) - ?.getTotalWithRewardsStakingBalance(blockchainId = currency.network.rawId).orZero() - private fun createTitleState( currencyStatus: CryptoCurrencyStatus, yieldModuleApyMap: Map, @@ -348,7 +324,9 @@ class TokenItemStateConverter( is CryptoCurrencyStatus.NoAccount, -> { TokenItemState.Subtitle2State.TextContent( - text = status.getFormattedCryptoAmount(), + text = status.getTotalCryptoAmount().format { + crypto(status.currency) + }, isFlickering = status.value.isFlickering(), ) } @@ -371,7 +349,12 @@ class TokenItemStateConverter( is CryptoCurrencyStatus.NoAccount, -> { TokenItemState.FiatAmountState.Content( - text = status.getFormattedFiatAmount(appCurrency = appCurrency), + text = status.getTotalFiatAmount().format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, isFlickering = status.value.isFlickering(), icons = buildList { if (status.value.yieldSupplyStatus?.isActive == true && diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index 171fb3f750..1a97b297ef 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) implementation(projects.libs.blockchainSdk) + implementation(projects.common) // endregion // region Project - Features API 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 a24dc8b95b..b1ecc1457c 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 @@ -3,13 +3,13 @@ package com.tangem.data.tokens.repository import com.tangem.blockchain.blockchains.ethereum.eip1559.isGaslessTxSupported import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider import com.tangem.blockchain.common.* +import com.tangem.common.getTotalStakingBalance import com.tangem.data.tokens.converters.UtxoConverter 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.staking.StakingBalance import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.staking.utils.getTotalStakingBalance import com.tangem.domain.tokens.model.CurrencyAmount import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 005eba3ef1..c2a83d7894 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -46,6 +46,7 @@ dependencies { implementation(projects.core.configToggles) implementation(projects.core.utils) implementation(projects.libs.crypto) + implementation(projects.common) /** Android - Other */ implementation(deps.androidx.paging.runtime) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFactory.kt index 14fb123230..987cc6b547 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFactory.kt @@ -2,16 +2,14 @@ package com.tangem.domain.tokens.operations import arrow.core.NonEmptyList import arrow.core.toNonEmptyListOrNull +import com.tangem.common.getTotalFiatAmount import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup -import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.utils.extensions.orZero -import java.math.BigDecimal /** * This factory creates a [TokenList] based on the provided list of [CryptoCurrencyStatus], [TokensGroupType], @@ -103,7 +101,7 @@ object TokenListFactory { if (hasLoading) return this sortedByDescending { group -> - group.currencies.sumOf { it.calculateBalance() } + group.currencies.sumOf { it.getTotalFiatAmount().orZero() } } } } @@ -117,16 +115,8 @@ object TokenListFactory { if (hasLoading) return this - sortedByDescending { it.calculateBalance() } + sortedByDescending { it.getTotalFiatAmount().orZero() } } } } - - private fun CryptoCurrencyStatus.calculateBalance(): BigDecimal { - val stakingBalance = value.stakingBalance as? StakingBalance.Data - val totalStakingBalance = stakingBalance?.getTotalWithRewardsStakingBalance(currency.network.rawId).orZero() - val totalFiatStakingBalance = totalStakingBalance.multiply(value.fiatRate.orZero()) - - return value.fiatAmount?.plus(totalFiatStakingBalance).orZero() - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculator.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculator.kt index db2f29b764..a36b6672f3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculator.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TotalFiatBalanceCalculator.kt @@ -2,13 +2,13 @@ package com.tangem.domain.tokens.operations import arrow.core.NonEmptyList import arrow.core.toNonEmptyListOrNull +import com.tangem.common.getTotalWithRewardsStakingBalance import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.getResultStatusSource import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.extensions.orZero import java.math.BigDecimal diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt index 44bf47cd76..7939ad13ad 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt @@ -1,13 +1,16 @@ package com.tangem.features.onramp.tokenlist.entity.utils +import com.tangem.common.getTotalCryptoAmount +import com.tangem.common.getTotalFiatAmount import com.tangem.common.ui.tokens.TokenItemStateConverter -import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedCryptoAmount -import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedFiatAmount import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference +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.currency.CryptoCurrencyStatus @@ -112,7 +115,9 @@ internal object OnrampTokenItemStateConverterFactory { is CryptoCurrencyStatus.NoAccount, -> { TokenItemState.Subtitle2State.TextContent( - text = status.getFormattedCryptoAmount(), + text = status.getTotalCryptoAmount().format { + crypto(cryptoCurrency = status.currency) + }, isFlickering = status.value.isFlickering(), ) } @@ -136,7 +141,12 @@ internal object OnrampTokenItemStateConverterFactory { is CryptoCurrencyStatus.NoAccount, -> { TokenItemState.FiatAmountState.TextContent( - text = status.getFormattedFiatAmount(appCurrency = appCurrency), + text = status.getTotalFiatAmount().format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, isAvailable = isAvailable, isFlickering = status.value.isFlickering(), ) diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/AccountAwardConverter.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/AccountAwardConverter.kt index e5ad207a29..cc1e154773 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/AccountAwardConverter.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/AccountAwardConverter.kt @@ -1,14 +1,17 @@ package com.tangem.feature.referral.model +import com.tangem.common.getTotalCryptoAmount +import com.tangem.common.getTotalFiatAmount import com.tangem.common.ui.account.CryptoPortfolioIconConverter import com.tangem.common.ui.account.PortfolioSelectUM import com.tangem.common.ui.account.toUM -import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedCryptoAmount -import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedFiatAmount import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.format.bigdecimal.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.AccountStatus import com.tangem.domain.models.currency.CryptoCurrency @@ -35,10 +38,17 @@ internal class AccountAwardConverter( iconState = CryptoCurrencyToIconStateConverter().convert(currency), titleState = TokenItemState.TitleState.Content(stringReference(currency.name)), fiatAmountState = TokenItemState.FiatAmountState.Content( - text = accountAwardToken.getFormattedFiatAmount(appCurrency = appCurrency), + text = accountAwardToken.getTotalFiatAmount().format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, ), subtitle2State = TokenItemState.Subtitle2State.TextContent( - text = accountAwardToken.getFormattedCryptoAmount(), + text = accountAwardToken.getTotalCryptoAmount().format { + crypto(cryptoCurrency = currency) + }, isFlickering = accountAwardToken.value.isFlickering(), ), subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(currency.symbol)), 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 a70dfc89f5..3ace74e0f1 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 @@ -5,6 +5,7 @@ import arrow.core.getOrElse import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.getValidatorsCount import com.tangem.common.routing.AppRouter import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData @@ -53,7 +54,6 @@ import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.repositories.P2PEthPoolRepository -import com.tangem.domain.staking.utils.getValidatorsCount import com.tangem.domain.tokens.* import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.* 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 4315d780ac..adcecd045b 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 @@ -1,15 +1,18 @@ package com.tangem.feature.swap.converters +import com.tangem.common.getTotalCryptoAmount +import com.tangem.common.getTotalFiatAmount import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter import com.tangem.common.ui.tokens.TokenItemStateConverter -import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedCryptoAmount -import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedFiatAmount import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference +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.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -108,7 +111,9 @@ internal class AccountTokenItemConverter( is CryptoCurrencyStatus.NoAccount, -> { TokenItemState.Subtitle2State.TextContent( - text = status.getFormattedCryptoAmount(), + text = status.getTotalCryptoAmount().format { + crypto(cryptoCurrency = status.currency) + }, isFlickering = status.value.isFlickering(), ) } @@ -132,7 +137,12 @@ internal class AccountTokenItemConverter( is CryptoCurrencyStatus.NoAccount, -> { TokenItemState.FiatAmountState.TextContent( - text = status.getFormattedFiatAmount(appCurrency = appCurrency), + text = status.getTotalFiatAmount().format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, isAvailable = isAvailable, isFlickering = status.value.isFlickering(), ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt index 0f9a0741ac..4bc8dfbebf 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsBalanceSelectStateConverter.kt @@ -1,12 +1,12 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory +import com.tangem.common.getTotalWithRewardsStakingBalance 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.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.StakingBalance -import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance import com.tangem.utils.Provider diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 44384391ba..0b10013609 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -1,6 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory import arrow.core.Either +import com.tangem.common.getTotalWithRewardsStakingBalance import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType @@ -10,7 +11,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.StakingBalance -import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType 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 ebf27e2456..4d2e2e0835 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 @@ -1,5 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory +import com.tangem.common.getRewardStakingBalance +import com.tangem.common.getTotalStakingBalance import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -13,8 +15,6 @@ import com.tangem.domain.models.staking.RewardBlockType import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo -import com.tangem.domain.staking.utils.getRewardStakingBalance -import com.tangem.domain.staking.utils.getTotalStakingBalance import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 4955e962ed..79ea585f1d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items +import com.tangem.common.getTotalWithRewardsStakingBalance import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.stringReference @@ -9,7 +10,6 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.StakingBalance -import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt index 2851d268b6..478351d5dc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt @@ -10,7 +10,7 @@ import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.StakingBalance -import com.tangem.domain.staking.utils.getTotalWithRewardsStakingBalance +import com.tangem.common.getTotalWithRewardsStakingBalance import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId From a96c3113d1577aaabdd4cdadddd9a1dfd4d4b2f5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Feb 2026 13:12:46 +0400 Subject: [PATCH 023/106] Updated on 2026-08-14 --- .../moonpay/MoonpayBlockchainMapping.kt | 1 + .../converter/BlockchainSDKConfigConverter.kt | 6 ++++++ .../environment/models/EnvironmentConfigModel.kt | 4 ++++ .../tangem/core/ui/extensions/BlockchainIcons.kt | 3 +++ core/ui/src/main/res/drawable/ic_monad_22.xml | 9 +++++++++ core/ui/src/main/res/drawable/img_monad_22.xml | 16 ++++++++++++++++ .../tangem/data/common/network/NetworkFactory.kt | 1 + .../onramp/legacy/MercuryoBlockchainMapping.kt | 1 + .../domain/card/configs/Wallet2CardConfig.kt | 2 ++ .../domain/card/configs/Wallet2CardConfigTest.kt | 2 ++ gradle/tangem_dependencies.toml | 2 +- .../com/tangem/blockchainsdk/utils/Blockchain.kt | 5 +++++ .../crypto/derivation/AccountNodeRecognizer.kt | 2 ++ 13 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 core/ui/src/main/res/drawable/ic_monad_22.xml create mode 100644 core/ui/src/main/res/drawable/img_monad_22.xml diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt index a42593ea9e..1ff346fa44 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt @@ -163,4 +163,5 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? Linea, LineaTestnet -> null ArbitrumNova -> null Plasma, PlasmaTestnet -> null + Monad, MonadTestnet -> null } \ 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 index ade515b41c..b9c88974ec 100644 --- 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 @@ -30,6 +30,10 @@ internal object BlockchainSDKConfigConverter : Converter R.drawable.img_linea_22 "arbitrum-nova" -> R.drawable.img_arbitrum_nova_22 "plasma", "plasma/test" -> R.drawable.img_plasma_22 + "monad", "monad/test" -> R.drawable.img_monad_22 else -> R.drawable.ic_alert_24 } } @@ -196,6 +197,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "linea", "linea/test" -> R.drawable.img_linea_22 "arbitrum-nova" -> R.drawable.img_arbitrum_nova_22 "plasma", "plasma/test" -> R.drawable.img_plasma_22 + "monad", "monad/test" -> R.drawable.img_monad_22 else -> R.drawable.ic_alert_24 } } @@ -296,6 +298,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "linea", "linea/test" -> R.drawable.ic_linea_22 "arbitrum-nova" -> R.drawable.ic_arbitrum_nova_22 "plasma", "plasma/test" -> R.drawable.ic_plasma_22 + "monad", "monad/test" -> R.drawable.ic_monad_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_monad_22.xml b/core/ui/src/main/res/drawable/ic_monad_22.xml new file mode 100644 index 0000000000..1e44a38e79 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_monad_22.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_monad_22.xml b/core/ui/src/main/res/drawable/img_monad_22.xml new file mode 100644 index 0000000000..9a471101c9 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_monad_22.xml @@ -0,0 +1,16 @@ + + + + + + + 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 265418339e..bf9014a844 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 @@ -376,6 +376,7 @@ class NetworkFactory @Inject constructor( Blockchain.Linea, Blockchain.LineaTestnet, Blockchain.ArbitrumNova, Blockchain.Plasma, Blockchain.PlasmaTestnet, + Blockchain.Monad, Blockchain.MonadTestnet, -> Network.TransactionExtrasType.NONE // endregion } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt index 9b9f34c2cf..c1bf85879a 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt @@ -163,5 +163,6 @@ public val Blockchain.mercuryoNetwork: String? Blockchain.Linea, Blockchain.LineaTestnet -> null Blockchain.ArbitrumNova -> null Blockchain.Plasma, Blockchain.PlasmaTestnet -> null + Blockchain.Monad, Blockchain.MonadTestnet -> null } } \ No newline at end of file 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 af64de970d..0c4ba1c0b0 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 @@ -217,6 +217,8 @@ data object Wallet2CardConfig : CardConfig { Blockchain.ArbitrumNova -> EllipticCurve.Secp256k1 Blockchain.Plasma -> EllipticCurve.Secp256k1 Blockchain.PlasmaTestnet -> EllipticCurve.Secp256k1 + Blockchain.Monad -> EllipticCurve.Secp256k1 + Blockchain.MonadTestnet -> EllipticCurve.Secp256k1 } } } \ No newline at end of file diff --git a/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt b/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt index 0e015b00af..9ef3985637 100644 --- a/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt +++ b/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt @@ -173,6 +173,8 @@ class Wallet2CardConfigTest { Blockchain.ArbitrumNova to EllipticCurve.Secp256k1, Blockchain.Plasma to EllipticCurve.Secp256k1, Blockchain.PlasmaTestnet to EllipticCurve.Secp256k1, + Blockchain.Monad to EllipticCurve.Secp256k1, + Blockchain.MonadTestnet to EllipticCurve.Secp256k1, ) @Test diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 1a6ce6fac6..1cc9fdda26 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-1408" +tangemBlockchainSdk = "develop-1419" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-577" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index 1e494e7fe8..216070afad 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -174,6 +174,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "arbitrum-nova" -> Blockchain.ArbitrumNova "plasma" -> Blockchain.Plasma "plasma/test" -> Blockchain.PlasmaTestnet + "monad" -> Blockchain.Monad + "monad/test" -> Blockchain.MonadTestnet else -> null } } @@ -345,6 +347,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.ArbitrumNova -> "arbitrum-nova" Blockchain.Plasma -> "plasma" Blockchain.PlasmaTestnet -> "plasma/test" + Blockchain.Monad -> "monad" + Blockchain.MonadTestnet -> "monad/test" } } @@ -453,6 +457,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Linea, Blockchain.LineaTestnet -> "linea-ethereum" Blockchain.ArbitrumNova -> "arbitrum-nova-ethereum" Blockchain.Plasma, Blockchain.PlasmaTestnet -> "plasma" + Blockchain.Monad, Blockchain.MonadTestnet -> "monad" } } 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 328bf4053e..32e90c0d4b 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 @@ -177,6 +177,7 @@ class AccountNodeRecognizer(private val blockchain: Blockchain) { Blockchain.ArbitrumNova, Blockchain.Quai, Blockchain.Plasma, + Blockchain.Monad, -> true Blockchain.Nexa, // unsupported network Blockchain.Chia, @@ -252,6 +253,7 @@ class AccountNodeRecognizer(private val blockchain: Blockchain) { Blockchain.QuaiTestnet, Blockchain.LineaTestnet, Blockchain.PlasmaTestnet, + Blockchain.MonadTestnet, -> false // endregion } From 57f3adf94effefdd4a329175fb9c471245a61aba Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Feb 2026 09:34:17 +0000 Subject: [PATCH 024/106] 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 2318c92aa6..1cc9fdda26 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.33-1413" +tangemBlockchainSdk = "develop-1419" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.33-576" +tangemCardSdk = "develop-577" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-541" +tangemHotSdk = "develop-539" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 0365d4cafc5ced6c89da52529e10bf3a1ca15891 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 8 Feb 2026 13:33:25 +0400 Subject: [PATCH 025/106] Updated on 2026-08-14 --- .../state/ExpressTransactionsBlockState.kt | 1 + .../EmptyExpressTransactionsComponent.kt | 1 + ...reviewEmptyExpressTransactionsComponent.kt | 1 + .../tangempay/ui/TangemPayDetailsScreen.kt | 2 +- .../model/ExpressTransactionsModel.kt | 76 +---- .../tokendetails/model/TokenDetailsModel.kt | 6 +- .../state/factory/ExpressStateFactory.kt | 60 ++++ .../factory/express/ExchangeStatusFactory.kt | 8 +- .../factory/express/ExpressStatusFactory.kt | 37 ++- .../factory/express/OnrampStatusFactory.kt | 8 +- .../TokenDetailsExchangeStatusFactory.kt | 272 ++++++++++++++++++ .../TokenDetailsExpressStatusFactory.kt | 216 ++++++++++++++ .../TokenDetailsOnrampStatusFactory.kt | 165 +++++++++++ 13 files changed, 762 insertions(+), 91 deletions(-) create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExpressStatusFactory.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsOnrampStatusFactory.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 870d30bc07..bd12e6323b 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 @@ -7,6 +7,7 @@ import kotlinx.collections.immutable.PersistentList data class ExpressTransactionsBlockState( val transactions: PersistentList, + val transactionsToDisplay: PersistentList, val bottomSheetSlot: BottomSheetSlot?, val dialogSlot: DialogSlot?, ) 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 a06c8a9f4f..e8eeac64b5 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 @@ -27,6 +27,7 @@ internal class EmptyExpressTransactionsComponent( private fun getInitialState(): ExpressTransactionsBlockState { return ExpressTransactionsBlockState( transactions = persistentListOf(), + transactionsToDisplay = persistentListOf(), bottomSheetSlot = null, dialogSlot = null, ) 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 23ead18dcf..ee4fe81f30 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 @@ -25,6 +25,7 @@ internal class PreviewEmptyExpressTransactionsComponent : ExpressTransactionsCom private fun getInitialState(): ExpressTransactionsBlockState { return ExpressTransactionsBlockState( transactions = persistentListOf(), + transactionsToDisplay = persistentListOf(), bottomSheetSlot = null, dialogSlot = null, ) 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 da34d0e786..11e52a71c4 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 @@ -149,7 +149,7 @@ internal fun TangemPayDetailsScreen( ) with(expressTransactionsComponent) { expressTransactionsContent( - state = expressState.transactions, + state = expressState.transactionsToDisplay, modifier = modifier .padding(start = 16.dp, end = 16.dp, top = 12.dp) .fillMaxWidth(), 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 6d5210c3cf..5e7a0d7096 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 @@ -1,19 +1,14 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.model -import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import arrow.core.getOrElse import arrow.core.right import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig -import com.tangem.common.ui.expressStatus.state.BottomSheetSlot -import com.tangem.common.ui.expressStatus.state.DialogSlot import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState -import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig 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.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -26,17 +21,12 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory +import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.ExpressStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.ExpressStatusFactory -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet import com.tangem.features.tokendetails.ExpressTransactionsComponent import com.tangem.features.tokendetails.ExpressTransactionsEvent import com.tangem.features.tokendetails.ExpressTransactionsEventListener -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import kotlinx.collections.immutable.PersistentList @@ -55,9 +45,7 @@ internal class ExpressTransactionsModel @Inject constructor( getUserWalletUseCase: GetUserWalletUseCase, private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val router: InnerTokenDetailsRouter, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, private val accountsFeatureToggles: AccountsFeatureToggles, private val getAccountCryptoCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val expressTransactionsEventListener: ExpressTransactionsEventListener, @@ -79,19 +67,12 @@ internal class ExpressTransactionsModel @Inject constructor( private val waitForFirstExpressStatusEmmit = MutableStateFlow(false) - private val currentStateProvider: Provider = Provider { internalUiState.value } + private val currentStateProvider: Provider = Provider { internalUiState.value } private val stateFactory by lazy(mode = LazyThreadSafetyMode.NONE) { - TokenDetailsStateFactory( - appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), + ExpressStateFactory( currentStateProvider = currentStateProvider, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - tokenDetailsClickIntents = EmptyTokenDetailsClickIntents(), expressTransactionsClickIntents = this, - networkHasDerivationUseCase = networkHasDerivationUseCase, - getUserWalletUseCase = getUserWalletUseCase, - userWalletId = userWalletId, - yieldSupplyFeatureToggles = yieldSupplyFeatureToggles, ) } @@ -106,15 +87,8 @@ internal class ExpressTransactionsModel @Inject constructor( ) } - private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency)) - + private val internalUiState = MutableStateFlow(stateFactory.getInitialState()) val uiState: StateFlow = internalUiState - .map(::mapInnerState) - .stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = mapInnerState(internalUiState.value), - ) init { subscribeOnExternalEvents() @@ -123,7 +97,7 @@ internal class ExpressTransactionsModel @Inject constructor( } override fun onExpressTransactionClick(txId: String) { - val expressTxState = internalUiState.value.expressTxsToDisplay.firstOrNull { it.info.txId == txId } + val expressTxState = internalUiState.value.transactionsToDisplay.firstOrNull { it.info.txId == txId } ?: return internalUiState.value = expressStatusFactory.getStateWithExpressStatusBottomSheet(expressTxState) } @@ -145,7 +119,7 @@ internal class ExpressTransactionsModel @Inject constructor( } override fun onDisposeExpressStatus() { - val bottomSheetState = internalUiState.value.bottomSheetConfig?.content + val bottomSheetState = internalUiState.value.bottomSheetSlot?.config?.content if (bottomSheetState is ExpressStatusBottomSheetConfig) { modelScope.launch { expressStatusFactory.removeTransactionOnBottomSheetClosed( @@ -158,7 +132,7 @@ internal class ExpressTransactionsModel @Inject constructor( } override fun onDismissBottomSheet() { - when (val bsContent = internalUiState.value.bottomSheetConfig?.content) { + when (val bsContent = internalUiState.value.bottomSheetSlot?.config?.content) { is ExpressStatusBottomSheetConfig -> { modelScope.launch(dispatchers.main) { expressStatusFactory.removeTransactionOnBottomSheetClosed(bsContent.value) @@ -177,35 +151,6 @@ internal class ExpressTransactionsModel @Inject constructor( super.onDestroy() } - private fun mapInnerState(innerState: TokenDetailsState): ExpressTransactionsBlockState { - val bsContent = innerState.bottomSheetConfig?.content - return ExpressTransactionsBlockState( - transactions = innerState.expressTxsToDisplay, - bottomSheetSlot = if (bsContent != null && bsContent is ExpressStatusBottomSheetConfig) { - innerState.bottomSheetConfig.toBottomSheetSlot() - } else { - null - }, - dialogSlot = innerState.dialogConfig?.toDialogSlot(), - ) - } - - private fun TangemBottomSheetConfig.toBottomSheetSlot(): BottomSheetSlot { - val contentLambda: @Composable () -> Unit = { - when (this.content) { - is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet(config = this) - } - } - return BottomSheetSlot(config = this, content = contentLambda) - } - - private fun TokenDetailsDialogConfig.toDialogSlot(): DialogSlot { - val contentLambda: @Composable () -> Unit = { - TokenDetailsDialogs(this) - } - return DialogSlot(config = this, content = contentLambda) - } - private fun subscribeOnExternalEvents() { modelScope.launch { expressTransactionsEventListener.event.collect { event -> @@ -232,10 +177,7 @@ internal class ExpressTransactionsModel @Inject constructor( } .distinctUntilChanged() .onEach { maybeCurrencyStatus -> - internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus) - maybeCurrencyStatus.onRight { status -> - cryptoCurrencyStatus = status - } + maybeCurrencyStatus.onRight { status -> cryptoCurrencyStatus = status } } .flowOn(dispatchers.main) .launchIn(modelScope) @@ -260,7 +202,7 @@ internal class ExpressTransactionsModel @Inject constructor( task = { try { Result.success( - expressStatusFactory.getUpdatedExpressStatuses(internalUiState.value.expressTxs), + expressStatusFactory.getUpdatedExpressStatuses(internalUiState.value.transactions), ) } catch (exception: CancellationException) { throw exception 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 a137a93bf5..f06bee0ec8 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 @@ -86,7 +86,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory -import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.ExpressStatusFactory +import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.TokenDetailsExpressStatusFactory import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokendetails.impl.R import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter @@ -137,7 +137,7 @@ internal class TokenDetailsModel @Inject constructor( @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter, paramsContainer: ParamsContainer, - expressStatusFactory: ExpressStatusFactory.Factory, + tokenDetailsExpressStatusFactory: TokenDetailsExpressStatusFactory.Factory, getUserWalletUseCase: GetUserWalletUseCase, private val appRouter: AppRouter, private val router: InnerTokenDetailsRouter, @@ -218,7 +218,7 @@ internal class TokenDetailsModel @Inject constructor( // endregion private val expressStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { - expressStatusFactory.create( + tokenDetailsExpressStatusFactory.create( clickIntents = this, appCurrencyProvider = Provider { selectedAppCurrencyFlow.value }, currentStateProvider = Provider { uiState.value }, 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 new file mode 100644 index 0000000000..e888745a6f --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt @@ -0,0 +1,60 @@ +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 { + return ExpressTransactionsBlockState( + transactions = persistentListOf(), + transactionsToDisplay = persistentListOf(), + bottomSheetSlot = null, + dialogSlot = null, + ) + } + + fun getStateWithClosedDialog(): ExpressTransactionsBlockState { + val state = currentStateProvider() + val slot = state.dialogSlot ?: return state + return state.copy(dialogSlot = slot.copy(config = slot.config.copy(isShow = false))) + } + + fun getStateWithClosedBottomSheet(): ExpressTransactionsBlockState { + val state = currentStateProvider() + val slot = state.bottomSheetSlot ?: return state + return state.copy(bottomSheetSlot = slot.copy(config = slot.config.copy(isShown = false))) + } + + 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/express/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt index d2b9ffaddb..778b4f2edf 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 @@ -1,6 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.local.swap.ExpressAnalyticsStatus import com.tangem.datasource.local.swap.SwapTransactionStatusStore @@ -19,7 +20,6 @@ import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsClickIntents -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter import com.tangem.utils.Provider @@ -46,7 +46,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( private val analyticsEventsHandler: AnalyticsEventHandler, @Assisted private val clickIntents: ExpressTransactionsClickIntents, @Assisted private val appCurrencyProvider: Provider, - @Assisted private val currentStateProvider: Provider, + @Assisted private val currentStateProvider: Provider, @Assisted private val userWallet: UserWallet, @Assisted private val cryptoCurrency: CryptoCurrency, ) { @@ -81,7 +81,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( suspend fun removeTransactionOnBottomSheetClosed(isForceDispose: Boolean = false) { val state = currentStateProvider() - val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return + val bottomSheetConfig = state.bottomSheetSlot?.config?.content as? ExpressStatusBottomSheetConfig ?: return val selectedTx = bottomSheetConfig.value as? ExchangeUM ?: return val shouldDispose = selectedTx.activeStatus?.isAutoDisposable == true || isForceDispose @@ -259,7 +259,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( fun create( clickIntents: ExpressTransactionsClickIntents, appCurrencyProvider: Provider, - currentStateProvider: Provider, + currentStateProvider: Provider, userWallet: UserWallet, cryptoCurrency: CryptoCurrency, ): ExchangeStatusFactory diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt index bb0dbbd4c4..7d3b0b6008 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt @@ -1,7 +1,10 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express +import androidx.compose.runtime.Composable import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.state.BottomSheetSlot import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM +import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.appcurrency.model.AppCurrency @@ -13,9 +16,9 @@ import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsClickIntents -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted @@ -32,7 +35,7 @@ import kotlinx.coroutines.withContext @Suppress("LongParameterList") internal class ExpressStatusFactory @AssistedInject constructor( - @Assisted private val currentStateProvider: Provider, + @Assisted private val currentStateProvider: Provider, @Assisted private val clickIntents: ExpressTransactionsClickIntents, @Assisted private val cryptoCurrency: CryptoCurrency, @Assisted appCurrencyProvider: Provider, @@ -93,9 +96,9 @@ internal class ExpressStatusFactory @AssistedInject constructor( fun getStateWithUpdatedExpressTxs( expressTxs: PersistentList, updateBalance: (CryptoCurrency) -> Unit, - ): TokenDetailsState { + ): ExpressTransactionsBlockState { val state = currentStateProvider() - val config = state.bottomSheetConfig + val config = state.bottomSheetSlot?.config val expressBottomSheet = config?.content as? ExpressStatusBottomSheetConfig val currentTx = expressTxs.firstOrNull { it.info.txId == expressBottomSheet?.value?.info?.txId } if (currentTx is ExchangeUM && currentTx.activeStatus == ExchangeStatus.Finished) { @@ -108,13 +111,14 @@ internal class ExpressStatusFactory @AssistedInject constructor( } }.toPersistentList() return state.copy( - expressTxs = expressTxs, - expressTxsToDisplay = expressTxsToDisplay, - bottomSheetConfig = currentTx?.let(::updateStateWithExpressStatusBottomSheet) ?: config, + transactions = expressTxs, + transactionsToDisplay = expressTxsToDisplay, + bottomSheetSlot = (currentTx?.let(::updateStateWithExpressStatusBottomSheet) ?: config) + ?.toBottomSheetSlot(), ) } - fun getStateWithExpressStatusBottomSheet(expressState: ExpressTransactionStateUM): TokenDetailsState { + fun getStateWithExpressStatusBottomSheet(expressState: ExpressTransactionStateUM): ExpressTransactionsBlockState { val analyticEvents = when (expressState) { is ExchangeUM -> listOfNotNull( TokenExchangeAnalyticsEvent.CexTxStatusOpened( @@ -139,19 +143,19 @@ internal class ExpressStatusFactory @AssistedInject constructor( analyticEvents.forEach { analyticsEventsHandler.send(it) } return currentStateProvider().copy( - bottomSheetConfig = TangemBottomSheetConfig( + bottomSheetSlot = TangemBottomSheetConfig( isShown = true, onDismissRequest = clickIntents::onDismissBottomSheet, content = ExpressStatusBottomSheetConfig( value = expressState, ), - ), + ).toBottomSheetSlot(), ) } fun updateStateWithExpressStatusBottomSheet(expressState: ExpressTransactionStateUM): TangemBottomSheetConfig? { val state = currentStateProvider() - val bottomSheetConfig = state.bottomSheetConfig + val bottomSheetConfig = state.bottomSheetSlot?.config val currentConfig = bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return bottomSheetConfig maybeGetLongTimeExchangeNotificationShowEvent( @@ -201,13 +205,22 @@ internal class ExpressStatusFactory @AssistedInject constructor( } } + private fun TangemBottomSheetConfig.toBottomSheetSlot(): BottomSheetSlot { + val contentLambda: @Composable () -> Unit = { + when (this.content) { + is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet(config = this) + } + } + return BottomSheetSlot(config = this, content = contentLambda) + } + @AssistedFactory interface Factory { @Suppress("LongParameterList") fun create( clickIntents: ExpressTransactionsClickIntents, appCurrencyProvider: Provider, - currentStateProvider: Provider, + currentStateProvider: Provider, userWallet: UserWallet, cryptoCurrency: CryptoCurrency, cryptoCurrencyStatusProvider: Provider, 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 4543119759..cbdf9089fa 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 @@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory. 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.analytics.api.AnalyticsEventHandler import com.tangem.datasource.local.swap.ExpressAnalyticsStatus import com.tangem.domain.appcurrency.model.AppCurrency @@ -16,7 +17,6 @@ import com.tangem.domain.onramp.model.OnrampStatus import com.tangem.domain.onramp.model.OnrampStatus.Status.* import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsClickIntents -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsOnrampTransactionStateConverter import com.tangem.utils.Provider import dagger.assisted.Assisted @@ -34,7 +34,7 @@ internal class OnrampStatusFactory @AssistedInject constructor( private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, private val onrampUpdateTransactionStatusUseCase: OnrampUpdateTransactionStatusUseCase, private val analyticsEventHandler: AnalyticsEventHandler, - @Assisted private val currentStateProvider: Provider, + @Assisted private val currentStateProvider: Provider, @Assisted private val cryptoCurrencyStatusProvider: Provider, @Assisted private val appCurrencyProvider: Provider, @Assisted private val clickIntents: ExpressTransactionsClickIntents, @@ -70,7 +70,7 @@ internal class OnrampStatusFactory @AssistedInject constructor( suspend fun removeTransactionOnBottomSheetClosed(isForceDispose: Boolean) { val state = currentStateProvider() - val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return + val bottomSheetConfig = state.bottomSheetSlot?.config?.content as? ExpressStatusBottomSheetConfig ?: return val selectedTx = bottomSheetConfig.value as? ExpressTransactionStateUM.OnrampUM ?: return if (selectedTx.activeStatus.isAutoDisposable || isForceDispose) { @@ -154,7 +154,7 @@ internal class OnrampStatusFactory @AssistedInject constructor( @AssistedFactory interface Factory { fun create( - currentStateProvider: Provider, + currentStateProvider: Provider, cryptoCurrencyStatusProvider: Provider, appCurrencyProvider: Provider, clickIntents: ExpressTransactionsClickIntents, 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 new file mode 100644 index 0000000000..7c10c7d5d6 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt @@ -0,0 +1,272 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express + +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.datasource.local.swap.ExpressAnalyticsStatus +import com.tangem.datasource.local.swap.SwapTransactionStatusStore +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase +import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.quote.QuoteStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.quotes.QuotesRepository +import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase +import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent +import com.tangem.feature.swap.domain.SwapTransactionRepository +import com.tangem.feature.swap.domain.api.SwapRepository +import com.tangem.feature.swap.domain.models.domain.* +import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsClickIntents +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter +import com.tangem.utils.Provider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.PersistentList +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 kotlin.coroutines.cancellation.CancellationException + +@Suppress("LongParameterList") +internal class TokenDetailsExchangeStatusFactory @AssistedInject constructor( + private val swapTransactionRepository: SwapTransactionRepository, + private val swapRepository: SwapRepository, + private val quotesRepository: QuotesRepository, + private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, + private val accountsFeatureToggles: AccountsFeatureToggles, + private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, + private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, + private val swapTransactionStatusStore: SwapTransactionStatusStore, + private val analyticsEventsHandler: AnalyticsEventHandler, + @Assisted private val clickIntents: ExpressTransactionsClickIntents, + @Assisted private val appCurrencyProvider: Provider, + @Assisted private val currentStateProvider: Provider, + @Assisted private val userWallet: UserWallet, + @Assisted private val cryptoCurrency: CryptoCurrency, +) { + + private val swapTransactionsStateConverter by lazy { + TokenDetailsSwapTransactionsStateConverter( + clickIntents = clickIntents, + cryptoCurrency = cryptoCurrency, + appCurrencyProvider = appCurrencyProvider, + analyticsEventsHandler = analyticsEventsHandler, + ) + } + + operator fun invoke(): Flow> { + return swapTransactionRepository.getTransactions( + userWallet = userWallet, + cryptoCurrencyId = cryptoCurrency.id, + ).conflate() + .map { savedTransactions -> + val quotes = savedTransactions + ?.flatMap { setOf(it.fromCryptoCurrency.id, it.toCryptoCurrency.id) } + ?.toSet() + ?.getQuotesOrEmpty() + .orEmpty() + + getExchangeStatusState( + savedTransactions = savedTransactions, + quoteStatuses = quotes, + ) + } + } + + suspend fun removeTransactionOnBottomSheetClosed(isForceDispose: Boolean = false) { + val state = currentStateProvider() + val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return + val selectedTx = bottomSheetConfig.value as? ExchangeUM ?: return + + val shouldDispose = selectedTx.activeStatus?.isAutoDisposable == true || isForceDispose + if (shouldDispose) { + swapTransactionRepository.removeTransaction( + userWalletId = userWallet.walletId, + txId = selectedTx.info.txId, + ) + } + } + + suspend fun updateSwapTxStatus(swapTx: ExchangeUM): ExchangeUM { + return if (swapTx.activeStatus?.isTerminal == true) { + swapTx + } else { + val statusModel = getExchangeStatus(swapTx.info.txId, swapTx.provider) + + if (statusModel != null) { + swapTransactionsStateConverter.updateTxStatus( + tx = swapTx, + statusModel = statusModel, + ) + } else { + swapTx + } + } + } + + private suspend fun getExchangeStatus(txId: String, provider: SwapProvider): ExchangeStatusModel? { + return swapRepository.getExchangeStatus(userWallet = userWallet, txId = txId) + .fold( + ifLeft = { null }, + ifRight = { statusModel -> + sendStatusUpdateAnalytics(statusModel, provider) + + val accountId = if (accountsFeatureToggles.isFeatureEnabled) { + getAccountCurrencyStatusUseCase.invokeSync( + userWalletId = userWallet.walletId, + currency = cryptoCurrency, + ) + .map { it.account.accountId } + .getOrNull() + } else { + null + } + + val refundTokenCurrency = if (accountsFeatureToggles.isFeatureEnabled) { + if (accountId != null) { + addRefundCurrencyIfNeededNew( + accountId = accountId, + status = statusModel, + type = provider.type, + ) + } else { + Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}") + null + } + } else { + addRefundCurrencyIfNeededLegacy(status = statusModel, type = provider.type) + } + + swapTransactionRepository.storeTransactionState( + txId = txId, + status = statusModel, + accountWithCurrency = if (refundTokenCurrency != null) { + Pair(accountId, refundTokenCurrency) + } else { + null + }, + ) + statusModel.copy(refundCurrency = refundTokenCurrency) + }, + ) + } + + private suspend fun sendStatusUpdateAnalytics(statusModel: ExchangeStatusModel, provider: SwapProvider) { + val txId = statusModel.txId ?: return + val status = toAnalyticStatus(statusModel.status) ?: return + val savedStatus = swapTransactionStatusStore.getTransactionStatus(txId) + + if (savedStatus != status) { + analyticsEventsHandler.send( + TokenExchangeAnalyticsEvent.CexTxStatusChanged(cryptoCurrency.symbol, status.value, provider.name), + ) + swapTransactionStatusStore.setTransactionStatus(txId, status) + } + } + + /** + * For now do it only for dex-bridge provider + */ + private suspend fun addRefundCurrencyIfNeededLegacy( + status: ExchangeStatusModel?, + type: ExchangeProviderType, + ): CryptoCurrency? { + status ?: return null + if (type != ExchangeProviderType.DEX_BRIDGE) return null + val refundNetwork = status.refundNetwork + val refundContractAddress = status.refundContractAddress + if (refundNetwork != null && refundContractAddress != null) { + return addCryptoCurrenciesUseCase( + userWalletId = userWallet.walletId, + contractAddress = refundContractAddress, + networkId = refundNetwork, + ).getOrNull() + } + return null + } + + private suspend fun addRefundCurrencyIfNeededNew( + accountId: AccountId, + status: ExchangeStatusModel?, + type: ExchangeProviderType, + ): CryptoCurrency? { + status ?: return null + if (type != ExchangeProviderType.DEX_BRIDGE) return null + val refundNetwork = status.refundNetwork + val refundContractAddress = status.refundContractAddress + + if (refundNetwork == null || refundContractAddress == null) return null + + return manageCryptoCurrenciesUseCase.add( + accountId = accountId, + contractAddress = refundContractAddress, + networkId = refundNetwork, + ) + .onLeft(Timber::e) + .getOrNull() + } + + private fun getExchangeStatusState( + savedTransactions: List?, + quoteStatuses: Set, + ): PersistentList { + if (savedTransactions == null) { + return persistentListOf() + } + + return swapTransactionsStateConverter.convert( + savedTransactions = savedTransactions, + quoteStatuses = quoteStatuses, + ) + } + + private fun toAnalyticStatus(status: ExchangeStatus?): ExpressAnalyticsStatus? { + return when (status) { + ExchangeStatus.New, + ExchangeStatus.Waiting, + ExchangeStatus.Sending, + ExchangeStatus.Confirming, + ExchangeStatus.Exchanging, + -> ExpressAnalyticsStatus.InProgress + ExchangeStatus.WaitingTxHash -> ExpressAnalyticsStatus.WaitingTxHash + ExchangeStatus.Verifying -> ExpressAnalyticsStatus.KYC + ExchangeStatus.Failed -> ExpressAnalyticsStatus.Fail + ExchangeStatus.TxFailed -> ExpressAnalyticsStatus.FailTx + ExchangeStatus.Finished -> ExpressAnalyticsStatus.Done + ExchangeStatus.Refunded -> ExpressAnalyticsStatus.Refunded + ExchangeStatus.Cancelled -> ExpressAnalyticsStatus.Cancelled + ExchangeStatus.Unknown -> ExpressAnalyticsStatus.Unknown + else -> null + } + } + + private suspend fun Set.getQuotesOrEmpty(): Set { + val rawIds = mapNotNull { it.rawCurrencyId }.toSet() + + return try { + quotesRepository.getMultiQuoteSyncOrNull(currenciesIds = rawIds).orEmpty() + } catch (exception: CancellationException) { + throw exception + } catch (ignore: Exception) { + emptySet() + } + } + + @AssistedFactory + interface Factory { + fun create( + clickIntents: ExpressTransactionsClickIntents, + appCurrencyProvider: Provider, + currentStateProvider: Provider, + userWallet: UserWallet, + cryptoCurrency: CryptoCurrency, + ): TokenDetailsExchangeStatusFactory + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExpressStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExpressStatusFactory.kt new file mode 100644 index 0000000000..19387b7150 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExpressStatusFactory.kt @@ -0,0 +1,216 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express + +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +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.UserWallet +import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent +import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent +import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent +import com.tangem.feature.swap.domain.models.domain.ExchangeStatus +import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsClickIntents +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotification +import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.withContext + +@Suppress("LongParameterList") +internal class TokenDetailsExpressStatusFactory @AssistedInject constructor( + @Assisted private val currentStateProvider: Provider, + @Assisted private val clickIntents: ExpressTransactionsClickIntents, + @Assisted private val cryptoCurrency: CryptoCurrency, + @Assisted appCurrencyProvider: Provider, + @Assisted userWallet: UserWallet, + @Assisted cryptoCurrencyStatusProvider: Provider, + private val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventsHandler: AnalyticsEventHandler, + tokenDetailsOnrampStatusFactory: TokenDetailsOnrampStatusFactory.Factory, + tokenDetailsExchangeStatusFactory: TokenDetailsExchangeStatusFactory.Factory, +) { + + private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { + tokenDetailsExchangeStatusFactory.create( + clickIntents = clickIntents, + appCurrencyProvider = appCurrencyProvider, + currentStateProvider = currentStateProvider, + userWallet = userWallet, + cryptoCurrency = cryptoCurrency, + ) + } + + private val onrampStatusFactory by lazy(LazyThreadSafetyMode.NONE) { + tokenDetailsOnrampStatusFactory.create( + currentStateProvider = currentStateProvider, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + appCurrencyProvider = appCurrencyProvider, + clickIntents = clickIntents, + cryptoCurrency = cryptoCurrency, + userWallet = userWallet, + ) + } + + fun getExpressStatuses(): Flow> = combine( + flow = exchangeStatusFactory(), + flow2 = onrampStatusFactory(), + ) { maybeExchange, maybeOnramp -> + persistentListOf(maybeOnramp, maybeExchange) + .flatten() + .sortedByDescending { it.info.timestamp } + .toPersistentList() + } + + suspend fun getUpdatedExpressStatuses(expressTxs: PersistentList) = + withContext(dispatchers.io) { + expressTxs.map { tx -> + async { + when (tx) { + is ExchangeUM -> exchangeStatusFactory.updateSwapTxStatus(tx) + is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.updateOnrmapTxStatus(tx) + else -> null + } + } + }.awaitAll() + .filterNotNull() + .toPersistentList() + } + + fun getStateWithUpdatedExpressTxs( + expressTxs: PersistentList, + updateBalance: (CryptoCurrency) -> Unit, + ): TokenDetailsState { + val state = currentStateProvider() + val config = state.bottomSheetConfig + val expressBottomSheet = config?.content as? ExpressStatusBottomSheetConfig + val currentTx = expressTxs.firstOrNull { it.info.txId == expressBottomSheet?.value?.info?.txId } + if (currentTx is ExchangeUM && currentTx.activeStatus == ExchangeStatus.Finished) { + updateBalance(currentTx.toCryptoCurrency) + } + val expressTxsToDisplay = expressTxs.filterNot { txs -> + when (txs) { + is ExpressTransactionStateUM.OnrampUM -> txs.activeStatus.isHidden + else -> false + } + }.toPersistentList() + return state.copy( + expressTxs = expressTxs, + expressTxsToDisplay = expressTxsToDisplay, + bottomSheetConfig = currentTx?.let(::updateStateWithExpressStatusBottomSheet) ?: config, + ) + } + + fun getStateWithExpressStatusBottomSheet(expressState: ExpressTransactionStateUM): TokenDetailsState { + val analyticEvents = when (expressState) { + is ExchangeUM -> listOfNotNull( + TokenExchangeAnalyticsEvent.CexTxStatusOpened( + cryptoCurrency.symbol, + ), + maybeGetLongTimeExchangeNotificationShowEvent( + expressState = expressState, + currentStateNotification = null, + isBottomSheetShown = true, + ), + ) + is ExpressTransactionStateUM.OnrampUM -> listOf( + TokenOnrampAnalyticsEvent.OnrampStatusOpened( + tokenSymbol = cryptoCurrency.symbol, + provider = expressState.providerName, + fiatCurrency = expressState.fromCurrencyCode, + ), + ) + else -> return currentStateProvider() + } + + analyticEvents.forEach { analyticsEventsHandler.send(it) } + + return currentStateProvider().copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = clickIntents::onDismissBottomSheet, + content = ExpressStatusBottomSheetConfig( + value = expressState, + ), + ), + ) + } + + fun updateStateWithExpressStatusBottomSheet(expressState: ExpressTransactionStateUM): TangemBottomSheetConfig? { + val state = currentStateProvider() + val bottomSheetConfig = state.bottomSheetConfig + val currentConfig = bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return bottomSheetConfig + + maybeGetLongTimeExchangeNotificationShowEvent( + expressState = expressState, + currentStateNotification = (currentConfig.value as? ExchangeUM)?.notification, + isBottomSheetShown = bottomSheetConfig.isShown, + )?.let { analyticsEventsHandler.send(it) } + + return bottomSheetConfig.copy( + content = if (currentConfig.value != expressState) { + ExpressStatusBottomSheetConfig(expressState) + } else { + currentConfig + }, + ) + } + + suspend fun removeTransactionOnBottomSheetClosed( + expressState: ExpressTransactionStateUM, + isForceDispose: Boolean = false, + ) { + when (expressState) { + is ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed(isForceDispose) + is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.removeTransactionOnBottomSheetClosed( + isForceDispose, + ) + } + } + + private fun maybeGetLongTimeExchangeNotificationShowEvent( + expressState: ExpressTransactionStateUM, + currentStateNotification: ExchangeStatusNotification?, + isBottomSheetShown: Boolean, + ): TokenScreenAnalyticsEvent? { + val newState = expressState as? ExchangeUM + val newStateNotification = newState?.notification + return if (currentStateNotification !is ExchangeStatusNotification.LongTimeExchange && + newStateNotification is ExchangeStatusNotification.LongTimeExchange && + isBottomSheetShown + ) { + TokenExchangeAnalyticsEvent.LongTimeTransaction( + token = cryptoCurrency.symbol, + provider = newState.provider.name, + ) + } else { + null + } + } + + @AssistedFactory + interface Factory { + @Suppress("LongParameterList") + fun create( + clickIntents: ExpressTransactionsClickIntents, + appCurrencyProvider: Provider, + currentStateProvider: Provider, + userWallet: UserWallet, + cryptoCurrency: CryptoCurrency, + cryptoCurrencyStatusProvider: Provider, + ): TokenDetailsExpressStatusFactory + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..802663f78a --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsOnrampStatusFactory.kt @@ -0,0 +1,165 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express + +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.datasource.local.swap.ExpressAnalyticsStatus +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.UserWallet +import com.tangem.domain.onramp.GetOnrampStatusUseCase +import com.tangem.domain.onramp.GetOnrampTransactionsUseCase +import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase +import com.tangem.domain.onramp.OnrampUpdateTransactionStatusUseCase +import com.tangem.domain.onramp.model.OnrampStatus +import com.tangem.domain.onramp.model.OnrampStatus.Status.* +import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent +import com.tangem.feature.tokendetails.presentation.tokendetails.model.ExpressTransactionsClickIntents +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsOnrampTransactionStateConverter +import com.tangem.utils.Provider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +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 TokenDetailsOnrampStatusFactory @AssistedInject constructor( + private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, + private val getOnrampStatusUseCase: GetOnrampStatusUseCase, + private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, + private val onrampUpdateTransactionStatusUseCase: OnrampUpdateTransactionStatusUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, + @Assisted private val currentStateProvider: Provider, + @Assisted private val cryptoCurrencyStatusProvider: Provider, + @Assisted private val appCurrencyProvider: Provider, + @Assisted private val clickIntents: ExpressTransactionsClickIntents, + @Assisted private val cryptoCurrency: CryptoCurrency, + @Assisted private val userWallet: UserWallet, +) { + + private val onrampTransactionStateConverter by lazy(LazyThreadSafetyMode.NONE) { + TokenDetailsOnrampTransactionStateConverter( + clickIntents = clickIntents, + cryptoCurrency = cryptoCurrency, + cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, + appCurrencyProvider = appCurrencyProvider, + analyticsEventHandler = analyticsEventHandler, + ) + } + + operator fun invoke(): Flow> { + return getOnrampTransactionsUseCase( + userWalletId = userWallet.walletId, + cryptoCurrencyId = cryptoCurrency.id, + ).map { maybeTransaction -> + maybeTransaction.fold( + ifRight = { onrampTxs -> + val transactions = onrampTransactionStateConverter.convertList(onrampTxs) + transactions.clearHiddenTerminal() + transactions + }, + ifLeft = { persistentListOf() }, + ) + } + } + + suspend fun removeTransactionOnBottomSheetClosed(isForceDispose: Boolean) { + val state = currentStateProvider() + val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return + val selectedTx = bottomSheetConfig.value as? ExpressTransactionStateUM.OnrampUM ?: return + + if (selectedTx.activeStatus.isAutoDisposable || isForceDispose) { + onrampRemoveTransactionUseCase(txId = selectedTx.info.txId) + } + } + + suspend fun updateOnrmapTxStatus(onrampTx: ExpressTransactionStateUM.OnrampUM): ExpressTransactionStateUM.OnrampUM { + return if (onrampTx.activeStatus.isTerminal) { + onrampTx + } else { + getOnrampStatusUseCase(userWallet = userWallet, onrampTx.info.txId).fold( + ifLeft = { error -> + Timber.e("Couldn't update onramp status. $error") + onrampTx + }, + ifRight = { statusModel -> + sendStatusUpdateAnalytics(onrampTx, statusModel) + onrampTx.copy( + activeStatus = statusModel.status, + info = onrampTx.info.copy( + txExternalId = statusModel.externalTxId, + txExternalUrl = statusModel.externalTxUrl, + ), + ) + }, + ) + } + } + + private suspend fun List.clearHiddenTerminal() { + this.filter { it.activeStatus.isHidden && it.activeStatus.isTerminal } + .forEach { onrampRemoveTransactionUseCase(txId = it.info.txId) } + } + + private suspend fun sendStatusUpdateAnalytics( + onrampTx: ExpressTransactionStateUM.OnrampUM, + statusModel: OnrampStatus, + ) { + val txId = statusModel.txId + val status = toAnalyticStatus(statusModel.status) ?: return + + if (statusModel.status != onrampTx.activeStatus) { + analyticsEventHandler.send( + TokenOnrampAnalyticsEvent.OnrampStatusChanged( + tokenSymbol = cryptoCurrency.symbol, + status = status.name, + provider = onrampTx.providerName, + fiatCurrency = onrampTx.fromCurrencyCode, + ), + ) + onrampUpdateTransactionStatusUseCase( + txId = txId, + externalTxUrl = statusModel.externalTxUrl.orEmpty(), + externalTxId = statusModel.externalTxId.orEmpty(), + status = statusModel.status, + ) + } + } + + private fun toAnalyticStatus(status: OnrampStatus.Status?): ExpressAnalyticsStatus? { + return when (status) { + Expired, + Paused, + -> ExpressAnalyticsStatus.Cancelled + Created, + WaitingForPayment, + PaymentProcessing, + Paid, + Sending, + RefundInProgress, + -> ExpressAnalyticsStatus.InProgress + Verifying -> ExpressAnalyticsStatus.KYC + Failed -> ExpressAnalyticsStatus.Fail + Finished -> ExpressAnalyticsStatus.Done + Refunded -> ExpressAnalyticsStatus.Refunded + null -> null + } + } + + @AssistedFactory + interface Factory { + fun create( + currentStateProvider: Provider, + cryptoCurrencyStatusProvider: Provider, + appCurrencyProvider: Provider, + clickIntents: ExpressTransactionsClickIntents, + cryptoCurrency: CryptoCurrency, + userWallet: UserWallet, + ): TokenDetailsOnrampStatusFactory + } +} \ No newline at end of file From b861d63402048afbbb21626419c5752e875d16bd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Feb 2026 12:22:08 +0200 Subject: [PATCH 026/106] Updated on 2026-08-14 --- .../com/tangem/scenarios/BaseScenarios.kt | 4 +- .../screens/MarketsTooltipPageObject.kt | 4 ++ .../core/ui/test/MarketTooltipTestTags.kt | 1 + core/ui/src/main/res/drawable/ic_plus_18.xml | 10 +++ .../wallet/child/wallet/model/WalletModel.kt | 13 +++- .../intents/WalletContentClickIntents.kt | 4 +- .../common/preview/WalletScreenPreviewData.kt | 2 +- .../wallet/state/WalletStateController.kt | 2 +- .../wallet/state/model/WalletScreenState.kt | 2 +- .../InitializeWalletsTransformer.kt | 2 +- .../presentation/wallet/ui/WalletScreen.kt | 68 +++++++++++++------ 11 files changed, 82 insertions(+), 30 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_plus_18.xml diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index e84ee3a24c..bc4f4593d7 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -65,8 +65,8 @@ fun BaseTestCase.openMainScreen( step("Assert 'Main' screen is displayed") { onMainScreen { screenContainer.assertIsDisplayed() } } - step("Click on 'Market Tooltip' screen") { - onMarketsTooltipScreen { contentContainer.clickWithAssertion() } + step("Dismiss Market Tooltip by clicking close button") { + onMarketsTooltipScreen { closeButton.clickWithAssertion() } } } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MarketsTooltipPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MarketsTooltipPageObject.kt index 08ae1af7d8..d66f0f80a8 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MarketsTooltipPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MarketsTooltipPageObject.kt @@ -13,6 +13,10 @@ class MarketsTooltipPageObject(semanticsProvider: SemanticsNodeInteractionsProvi val contentContainer: KNode = child { hasTestTag(MarketTooltipTestTags.CONTAINER) } + + val closeButton: KNode = child { + hasTestTag(MarketTooltipTestTags.CLOSE_BUTTON) + } } internal fun BaseTestCase.onMarketsTooltipScreen(function: MarketsTooltipPageObject.() -> Unit) = diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/MarketTooltipTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/MarketTooltipTestTags.kt index 383bc099fd..77ff36a35d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/MarketTooltipTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/MarketTooltipTestTags.kt @@ -2,4 +2,5 @@ package com.tangem.core.ui.test object MarketTooltipTestTags { const val CONTAINER = "MARKETS_TOOLTIP_CONTAINER" + const val CLOSE_BUTTON = "MARKETS_TOOLTIP_CLOSE_BUTTON" } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_plus_18.xml b/core/ui/src/main/res/drawable/ic_plus_18.xml new file mode 100644 index 0000000000..cc0ffddd36 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_plus_18.xml @@ -0,0 +1,10 @@ + + + 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 b75d8ebaea..d1bd0b0529 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 @@ -146,6 +146,17 @@ internal class WalletModel @Inject constructor( fun onResume() { suggestToEnableBiometrics() + suggestToOpenMarketsOnResume() + } + + private fun suggestToOpenMarketsOnResume() { + modelScope.launch { + if (shouldShowMarketsTooltipUseCase()) { + stateHolder.update { + it.copy(showMarketsOnboarding = true) + } + } + } } private fun updateMarketToggle() { @@ -203,8 +214,6 @@ internal class WalletModel @Inject constructor( it.copy(showMarketsOnboarding = true) } } - - shouldShowMarketsTooltipUseCase(isShown = true) } } 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 59c28e1d6d..f4769cd5bf 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 @@ -46,7 +46,7 @@ internal interface WalletContentClickIntents { fun onOrganizeTokensClick() - fun onDismissMarketsOnboarding() + fun onDismissMarketsTooltip() fun onTokenItemClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) @@ -112,7 +112,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) } - override fun onDismissMarketsOnboarding() { + override fun onDismissMarketsTooltip() { stateHolder.update { it.copy(showMarketsOnboarding = false) } modelScope.launch { shouldShowMarketsTooltipUseCase(isShown = true) 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 e617d2d783..59f9ca6314 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 @@ -222,7 +222,7 @@ internal object WalletScreenPreviewData { event = consumedEvent(), isHidingMode = false, showMarketsOnboarding = false, - onDismissMarketsOnboarding = {}, + onDismissMarketsTooltip = {}, isNewMarketEnabled = false, ) 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 572e187d6b..a39274838f 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 @@ -109,7 +109,7 @@ internal class WalletStateController @Inject constructor() { event = consumedEvent(), isHidingMode = false, showMarketsOnboarding = false, - onDismissMarketsOnboarding = {}, + onDismissMarketsTooltip = {}, isNewMarketEnabled = false, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt index 1fb6a6d24c..9c6f2658ae 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt @@ -14,5 +14,5 @@ internal data class WalletScreenState( val isHidingMode: Boolean, val showMarketsOnboarding: Boolean, val isNewMarketEnabled: Boolean, - val onDismissMarketsOnboarding: () -> Unit, + val onDismissMarketsTooltip: () -> Unit, ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index 200f8be43d..559a45db9d 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 @@ -44,7 +44,7 @@ internal class InitializeWalletsTransformer( } .toImmutableList(), onWalletChange = clickIntents::onWalletChange, - onDismissMarketsOnboarding = clickIntents::onDismissMarketsOnboarding, + onDismissMarketsTooltip = clickIntents::onDismissMarketsTooltip, ) } 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 a9f5d27170..ec373aea09 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 @@ -409,19 +409,22 @@ private inline fun BaseScaffoldWithMarkets( visible = bottomSheetState.targetValue == TangemSheetValue.Expanded || state.showMarketsOnboarding, onDismissRequest = { - coroutineScope.launch { bottomSheetState.partialExpand() } - state.onDismissMarketsOnboarding() + if (!state.showMarketsOnboarding) { + coroutineScope.launch { bottomSheetState.partialExpand() } + } }, ) MarketsTooltip( modifier = Modifier .align(Alignment.BottomCenter) - .padding(bottom = 24.dp) - .fillMaxWidth(fraction = 0.7f), + .padding(bottom = 8.dp) + .padding(horizontal = 16.dp) + .fillMaxWidth(), isVisible = state.showMarketsOnboarding, availableHeight = maxHeight, bottomSheetState = bottomSheetState, + onCloseClick = state.onDismissMarketsTooltip, ) } }, @@ -443,7 +446,7 @@ private inline fun BaseScaffoldWithMarkets( LaunchedEffect(state.showMarketsOnboarding, bottomSheetState.targetValue) { if (state.showMarketsOnboarding && bottomSheetState.targetValue == TangemSheetValue.Expanded) { - state.onDismissMarketsOnboarding() + state.onDismissMarketsTooltip() } } } @@ -454,6 +457,7 @@ private fun MarketsTooltip( availableHeight: Dp, bottomSheetState: TangemSheetState, isVisible: Boolean, + onCloseClick: () -> Unit, modifier: Modifier = Modifier, ) { val density = LocalDensity.current @@ -494,7 +498,7 @@ private fun MarketsTooltip( ) + fadeIn(), exit = fadeOut(), ) { - MarketsTooltipContent() + MarketsTooltipContent(onCloseClick = onCloseClick) } } @@ -527,12 +531,12 @@ internal fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier) { } @Composable -private fun MarketsTooltipContent(modifier: Modifier = Modifier) { - val backgroundColor = TangemTheme.colors.background.action - val cornerRadius = CornerRadius(x = 14.dp.toPx()) +private fun MarketsTooltipContent(onCloseClick: () -> Unit, modifier: Modifier = Modifier) { + val backgroundColor = TangemTheme.colors.background.primary + val cornerRadius = CornerRadius(x = 16.dp.toPx()) val tipDpSize = DpSize(width = 20.dp, height = 8.dp) - Column( + Row( modifier = modifier .padding(bottom = tipDpSize.height) .drawBehind { @@ -555,18 +559,42 @@ private fun MarketsTooltipContent(modifier: Modifier = Modifier) { drawPath(color = backgroundColor, path = tipPath) } .padding(all = 12.dp), - verticalArrangement = Arrangement.spacedBy(space = 4.dp), - horizontalAlignment = Alignment.Start, + horizontalArrangement = Arrangement.spacedBy(space = 12.dp), + verticalAlignment = Alignment.Top, ) { - Text( - text = stringResourceSafe(id = R.string.markets_tooltip_title), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, + Icon( + modifier = Modifier.size(size = 18.dp), + painter = painterResource(id = R.drawable.ic_plus_18), + tint = Color.Unspecified, + contentDescription = null, ) - Text( - text = stringResourceSafe(id = R.string.markets_tooltip_message), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.secondary, + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(space = 2.dp), + ) { + Text( + text = stringResourceSafe(id = R.string.markets_tooltip_v2_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = stringResourceSafe(id = R.string.markets_tooltip_message), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + } + Icon( + modifier = Modifier + .size(size = 16.dp) + .clickable( + interactionSource = null, + indication = null, + onClick = onCloseClick, + ) + .testTag(MarketTooltipTestTags.CLOSE_BUTTON), + painter = painterResource(id = R.drawable.ic_close_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, ) } } From 652009a7bfb61d01aa6cc8e65ef9eeea3438a880 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 17:41:42 +0400 Subject: [PATCH 027/106] Updated on 2026-08-14 --- .../tap/di/domain/AccountDomainModule.kt | 3 + .../DefaultUserWalletsListRepository.kt | 32 +++++--- .../AccountConverterFactoryContainer.kt | 12 +-- .../data/account/di/AccountDataModule.kt | 3 - .../fetcher/DefaultMultiAccountListFetcher.kt | 10 ++- .../AccountListCryptoCurrenciesProducer.kt | 8 +- .../DefaultMultiAccountListProducer.kt | 9 ++- ...aultMultiWalletCryptoCurrenciesProducer.kt | 9 ++- .../producer/WalletAccountListFlowFactory.kt | 7 +- .../DefaultAccountsCRUDRepository.kt | 11 --- .../DefaultWalletAccountsResponseFactory.kt | 8 +- .../DefaultMultiAccountListFetcherTest.kt | 19 +++-- .../DefaultMultiAccountListProducerTest.kt | 42 +++++------ ...MultiWalletCryptoCurrenciesProducerTest.kt | 34 ++++----- .../WalletAccountListFlowFactoryTest.kt | 14 ++-- .../DefaultAccountsCRUDRepositoryTest.kt | 3 - ...efaultWalletAccountsResponseFactoryTest.kt | 20 ++--- domain/account/build.gradle.kts | 1 + .../repository/AccountsCRUDRepository.kt | 15 ---- .../usecase/IsAccountsModeEnabledUseCase.kt | 11 ++- .../IsAccountsModeEnabledUseCaseTest.kt | 64 +++++++++------- .../status/di/AccountStatusUseCaseModule.kt | 9 ++- .../DefaultMultiAccountStatusListProducer.kt | 9 ++- .../DefaultSingleAccountStatusListProducer.kt | 18 +++-- .../GetAccountCurrencyByAddressUseCase.kt | 8 +- .../GetCryptoCurrencyActionsUseCaseV2.kt | 8 +- ...faultMultiAccountStatusListProducerTest.kt | 55 +++++++------- .../GetAccountCurrencyByAddressUseCaseTest.kt | 75 +++++++++---------- .../wallets/UserWalletsListRepository.kt | 10 +++ 29 files changed, 271 insertions(+), 256 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index 80d510d122..569c69c6b8 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -9,6 +9,7 @@ import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher import com.tangem.domain.account.tokens.MainAccountTokensMigration import com.tangem.domain.account.usecase.* +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.feature.referral.data.ExternalReferralRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -94,10 +95,12 @@ internal object AccountDomainModule { @Provides @Singleton fun provideIsAccountsModeEnabledUseCase( + userWalletsListRepository: UserWalletsListRepository, accountsCRUDRepository: AccountsCRUDRepository, accountsFeatureToggles: AccountsFeatureToggles, ): IsAccountsModeEnabledUseCase { return IsAccountsModeEnabledUseCase( + userWalletsListRepository = userWalletsListRepository, crudRepository = accountsCRUDRepository, accountsFeatureToggles = accountsFeatureToggles, ) diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index f307b8653d..9aee042a1c 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -30,19 +30,16 @@ import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.model.HotWalletId import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey -import com.tangem.tap.domain.userWalletList.utils.encryptionKey -import com.tangem.tap.domain.userWalletList.utils.lock -import com.tangem.tap.domain.userWalletList.utils.publicInformation -import com.tangem.tap.domain.userWalletList.utils.sensitiveInformation -import com.tangem.tap.domain.userWalletList.utils.toUserWallets -import com.tangem.tap.domain.userWalletList.utils.updateWith +import com.tangem.tap.domain.userWalletList.utils.* import com.tangem.utils.Provider import com.tangem.utils.ProviderSuspend import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.extensions.addOrReplace import com.tangem.utils.extensions.indexOfFirstOrNull import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.update import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -67,8 +64,17 @@ internal class DefaultUserWalletsListRepository( override val userWallets = MutableStateFlow?>(null) override val selectedUserWallet = MutableStateFlow(null) + private val mutex = Mutex() + override fun getSyncOrNull(id: UserWalletId): UserWallet? { + return userWallets.value?.find { it.walletId == id } + } + + override fun getSyncStrict(id: UserWalletId): UserWallet { + return requireNotNull(getSyncOrNull(id)) { "Unable to find user wallet with provided ID: $id" } + } + override suspend fun load() { mutex.withLock { if (userWallets.value != null) return @@ -103,6 +109,13 @@ internal class DefaultUserWalletsListRepository( } } + override fun loadAndGet(): Flow> = flow { + load() + userWallets.collect { + emit(requireNotNull(it)) + } + } + override suspend fun userWalletsSync(): List { load() return requireNotNull(userWallets.value) { @@ -289,8 +302,7 @@ internal class DefaultUserWalletsListRepository( } val scanResponse = unlockMethod.scanResponse ?: run { - val res = tangemSdkManagerProvider().scanProduct() - when (res) { + when (val res = tangemSdkManagerProvider().scanProduct()) { is CompletionResult.Failure -> raise(UnlockWalletError.UserCancelled) is CompletionResult.Success -> res.data } @@ -467,9 +479,7 @@ internal class DefaultUserWalletsListRepository( authMode = true, // In auth mode user wallet can be deleted after 30 failed attempts hasBiometry = hasBiometry(), ) - val result = passwordRequester.requestPassword(attemptRequest) - - return when (result) { + return when (val result = passwordRequester.requestPassword(attemptRequest)) { HotWalletPasswordRequester.Result.Dismiss -> { passwordRequester.dismiss() UnlockWalletError.UserCancelled.left() diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt index 8a7f2de408..95de2c8111 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt @@ -1,6 +1,6 @@ package com.tangem.data.account.converter -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWalletId import javax.inject.Inject @@ -10,7 +10,7 @@ import javax.inject.Inject * @property getWalletAccountsResponseCF factory for creating a wallet accounts response converter * @property accountsListCF factory for creating an account list converter * @property cryptoPortfolioCF factory for creating a crypto portfolio converter - * @property userWalletsStore store for accessing user wallet data + * @property userWalletsListRepository repository for getting user wallets * * @constructor Creates an instance of the container with injected factories. * @@ -20,23 +20,23 @@ internal class AccountConverterFactoryContainer @Inject constructor( private val getWalletAccountsResponseCF: GetWalletAccountsResponseConverter.Factory, private val accountsListCF: AccountListConverter.Factory, private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, ) { fun createWalletAccountsResponseConverter(userWalletId: UserWalletId): GetWalletAccountsResponseConverter { - val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = userWalletId) return getWalletAccountsResponseCF.create(userWallet) } fun createAccountListConverter(userWalletId: UserWalletId): AccountListConverter { - val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = userWalletId) return accountsListCF.create(userWallet) } fun createCryptoPortfolioConverter(userWalletId: UserWalletId): CryptoPortfolioConverter { - val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = userWalletId) return cryptoPortfolioCF.create(userWallet) } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt index 559ba4c6ce..fc62ab079a 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt @@ -22,7 +22,6 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.accounts.AccountTokenMigrationStore import com.tangem.datasource.local.datastore.RuntimeStateStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.mapWithStringKeyTypes import com.tangem.datasource.utils.setTypes @@ -56,7 +55,6 @@ internal object AccountDataModule { tangemTechApi: TangemTechApi, walletAccountsSaver: WalletAccountsSaver, accountsResponseStoreFactory: AccountsResponseStoreFactory, - userWalletsStore: UserWalletsStore, userTokensSaver: UserTokensSaver, accountConverterFactoryContainer: AccountConverterFactoryContainer, @ApplicationContext context: Context, @@ -67,7 +65,6 @@ internal object AccountDataModule { walletAccountsSaver = walletAccountsSaver, accountsResponseStoreFactory = accountsResponseStoreFactory, archivedAccountsStoreFactory = ArchivedAccountsStoreFactory, - userWalletsStore = userWalletsStore, userTokensSaver = userTokensSaver, archivedAccountsETagStore = RuntimeStateStore(emptyMap()), convertersContainer = accountConverterFactoryContainer, 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 2be1ac75dc..b655bbbdcc 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 @@ -2,9 +2,9 @@ package com.tangem.data.account.fetcher import arrow.core.Either import arrow.core.raise.either -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.fetcher.MultiAccountListFetcher 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 kotlinx.coroutines.coroutineScope @@ -17,13 +17,13 @@ import javax.inject.Inject * Implementation of [MultiAccountListFetcher] * * @property singleAccountListFetcher instance of [SingleAccountListFetcher] to fetch accounts for a single wallet - * @property userWalletsStore instance of [UserWalletsStore] to get all user wallets + * @property userWalletsListRepository repository for getting user wallets * [REDACTED_AUTHOR] */ internal class DefaultMultiAccountListFetcher @Inject constructor( private val singleAccountListFetcher: SingleAccountListFetcher, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, ) : MultiAccountListFetcher { override suspend fun invoke(params: MultiAccountListFetcher.Params): Either = either { @@ -58,7 +58,9 @@ internal class DefaultMultiAccountListFetcher @Inject constructor( } } MultiAccountListFetcher.Params.All -> { - val userWalletsIds = userWalletsStore.userWalletsSync.map(UserWallet::walletId).toSet() + val userWalletsIds = userWalletsListRepository.userWallets.value.orEmpty() + .map(UserWallet::walletId) + .toSet() invoke(params = MultiAccountListFetcher.Params.Set(ids = userWalletsIds)).bind() } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt index 12ad2e3ef9..305d08e560 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt @@ -4,7 +4,7 @@ import arrow.core.Option import arrow.core.some import com.tangem.data.account.store.AccountsResponseStoreFactory import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency @@ -20,7 +20,7 @@ import kotlinx.coroutines.flow.* * Implementation of [MultiWalletCryptoCurrenciesProducer] that produces crypto currencies of all accounts * * @property params params - * @property userWalletsStore UserWallet's store + * @property userWalletsListRepository repository for getting user wallets * @property accountsResponseStoreFactory factory to create store with accounts response * @property responseCryptoCurrenciesFactory factory for creating [CryptoCurrency] from `UserTokensResponse` * @property dispatchers dispatchers @@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.* */ internal class AccountListCryptoCurrenciesProducer @AssistedInject constructor( @Assisted val params: MultiWalletCryptoCurrenciesProducer.Params, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val accountsResponseStoreFactory: AccountsResponseStoreFactory, private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, override val flowProducerTools: FlowProducerTools, @@ -40,7 +40,7 @@ internal class AccountListCryptoCurrenciesProducer @AssistedInject constructor( @Suppress("NullableToStringCall") override fun produce(): Flow> { - val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) if (!userWallet.isMultiCurrency) { error("${this::class.simpleName ?: this::class.toString()} supports only multi-currency wallet") diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt index 2bab825935..39dc1c5d19 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt @@ -2,9 +2,9 @@ package com.tangem.data.account.producer import arrow.core.Option import arrow.core.some -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.MultiAccountListProducer +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.wallet.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -19,7 +19,8 @@ import kotlinx.coroutines.flow.* * Produces a list of [AccountList]s for all user wallets. * * @property params params - * @property userWalletsStore store that provides user wallets + * @property flowProducerTools tools for producing flows + * @property userWalletsListRepository repository for getting user wallets * @property walletAccountListFlowFactory builder to create flows of [AccountList] for each wallet * @property dispatchers coroutine dispatchers provider * @@ -28,7 +29,7 @@ import kotlinx.coroutines.flow.* internal class DefaultMultiAccountListProducer @AssistedInject constructor( @Assisted val params: Unit, override val flowProducerTools: FlowProducerTools, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val walletAccountListFlowFactory: WalletAccountListFlowFactory, private val dispatchers: CoroutineDispatcherProvider, ) : MultiAccountListProducer { @@ -37,7 +38,7 @@ internal class DefaultMultiAccountListProducer @AssistedInject constructor( @OptIn(ExperimentalCoroutinesApi::class) override fun produce(): Flow> { - return userWalletsStore.userWallets + return userWalletsListRepository.loadAndGet() .map { it.map(UserWallet::walletId) } .distinctUntilChanged() .flatMapLatest { ids -> diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducer.kt index 8b82206cdb..6476babc80 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducer.kt @@ -4,7 +4,7 @@ import arrow.core.Option import arrow.core.some import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency @@ -20,7 +20,8 @@ import kotlinx.coroutines.flow.* * Default implementation of [MultiWalletCryptoCurrenciesProducer] * * @property params params - * @property userWalletsStore UserWallet's store + * @property flowProducerTools tools for producing flows + * @property userWalletsListRepository repository for getting user wallets * @property userTokensResponseStore store of `UserTokensResponse` * @property responseCryptoCurrenciesFactory factory for creating [CryptoCurrency] from `UserTokensResponse` * @property dispatchers dispatchers @@ -30,7 +31,7 @@ import kotlinx.coroutines.flow.* internal class DefaultMultiWalletCryptoCurrenciesProducer @AssistedInject constructor( @Assisted val params: MultiWalletCryptoCurrenciesProducer.Params, override val flowProducerTools: FlowProducerTools, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val userTokensResponseStore: UserTokensResponseStore, private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, private val dispatchers: CoroutineDispatcherProvider, @@ -39,7 +40,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducer @AssistedInject constr override val fallback: Option> = emptySet().some() override fun produce(): Flow> { - val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) if (!userWallet.isMultiCurrency) { error("${this::class.simpleName ?: this::class.toString()} supports only multi-currency wallet") diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt index 44aa8713fc..624eeb6679 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt @@ -4,9 +4,9 @@ import com.tangem.data.account.converter.AccountListConverter import com.tangem.data.account.store.AccountsResponseStore import com.tangem.data.account.store.AccountsResponseStoreFactory import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency @@ -17,6 +17,7 @@ import javax.inject.Inject /** * Factory that creates a flow of [AccountList] for a specific [UserWallet] * + * @property userWalletsListRepository repository to get user wallets * @property accountsResponseStoreFactory factory to create [AccountsResponseStore] * @property accountListConverterFactory factory to create [AccountListConverter] * @property cardCryptoCurrencyFactory factory to create supported crypto currencies for a card @@ -24,14 +25,14 @@ import javax.inject.Inject [REDACTED_AUTHOR] */ internal class WalletAccountListFlowFactory @Inject constructor( - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val accountsResponseStoreFactory: AccountsResponseStoreFactory, private val accountListConverterFactory: AccountListConverter.Factory, private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, ) { fun create(userWalletId: UserWalletId): Flow { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) return if (userWallet.isMultiCurrency) { createForMultiWallet(userWallet) 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 2bb36f1808..ef589cc97d 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 @@ -22,7 +22,6 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse import com.tangem.datasource.local.datastore.RuntimeStateStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.utils.getSyncOrNull import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.ArchivedAccount @@ -30,7 +29,6 @@ import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.replaceBy @@ -48,7 +46,6 @@ internal class DefaultAccountsCRUDRepository( private val walletAccountsSaver: WalletAccountsSaver, private val accountsResponseStoreFactory: AccountsResponseStoreFactory, private val archivedAccountsStoreFactory: ArchivedAccountsStoreFactory, - private val userWalletsStore: UserWalletsStore, private val userTokensSaver: UserTokensSaver, private val archivedAccountsETagStore: RuntimeStateStore>, private val convertersContainer: AccountConverterFactoryContainer, @@ -205,14 +202,6 @@ internal class DefaultAccountsCRUDRepository( .map { it?.accounts?.size.toOption() } } - override fun getUserWallet(userWalletId: UserWalletId): UserWallet { - return userWalletsStore.getSyncStrict(userWalletId) - } - - override fun getUserWallets(): Flow> = userWalletsStore.userWallets - - override fun getUserWalletsSync(): List = userWalletsStore.userWalletsSync - override fun checkDefaultAccountName(accountList: AccountList, accountName: AccountName) { val hasDefaultName = accountList.accounts.any { it.accountName is AccountName.DefaultMain } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt index 023e9161ec..e49529c64f 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt @@ -6,8 +6,8 @@ import com.tangem.data.common.network.NetworkFactory import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.DerivationIndex @@ -18,7 +18,7 @@ import javax.inject.Inject /** * Factory to create default [GetWalletAccountsResponse]. * - * @property userWalletsStore store to get user wallet information + * @property userWalletsListRepository repository to get user wallets * @property cryptoPortfolioCF converter factory to convert crypto portfolio accounts * @property userTokensResponseFactory factory to create [UserTokensResponse] * @property networkFactory factory to create network derivation path @@ -26,14 +26,14 @@ import javax.inject.Inject [REDACTED_AUTHOR] */ internal class DefaultWalletAccountsResponseFactory @Inject constructor( - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory, private val userTokensResponseFactory: UserTokensResponseFactory, private val networkFactory: NetworkFactory, ) { fun create(userWalletId: UserWalletId, userTokensResponse: UserTokensResponse?): GetWalletAccountsResponse { - val userWallet = userWalletsStore.getSyncOrNull(userWalletId) + val userWallet = userWalletsListRepository.getSyncOrNull(userWalletId) val accountDTOs = userWallet?.let(::createDefaultAccountDTOs).orEmpty() val response = userTokensResponse.orDefault(userWallet = userWallet) diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcherTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcherTest.kt index a2971c7891..8606110353 100644 --- a/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcherTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultMultiAccountListFetcherTest.kt @@ -2,9 +2,9 @@ package com.tangem.data.account.fetcher import arrow.core.left import arrow.core.right -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.fetcher.MultiAccountListFetcher 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.test.core.assertEitherLeft @@ -19,9 +19,12 @@ import org.junit.jupiter.api.TestInstance class DefaultMultiAccountListFetcherTest { private val singleAccountListFetcher: SingleAccountListFetcher = mockk() - private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true) + private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) - private val fetcher = DefaultMultiAccountListFetcher(singleAccountListFetcher, userWalletsStore) + private val fetcher = DefaultMultiAccountListFetcher( + singleAccountListFetcher = singleAccountListFetcher, + userWalletsListRepository = userWalletsListRepository, + ) private val userWalletId1 = UserWalletId("011") private val userWalletId2 = UserWalletId("012") @@ -35,7 +38,7 @@ class DefaultMultiAccountListFetcherTest { @AfterEach fun tearDown() { - clearMocks(singleAccountListFetcher, userWalletsStore) + clearMocks(singleAccountListFetcher, userWalletsListRepository) } @Test @@ -98,7 +101,7 @@ class DefaultMultiAccountListFetcherTest { // Arrange val params = MultiAccountListFetcher.Params.All - every { userWalletsStore.userWalletsSync } returns listOf(userWallets.first()) + every { userWalletsListRepository.userWallets.value } returns listOf(userWallets.first()) coEvery { singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId1)) @@ -111,7 +114,7 @@ class DefaultMultiAccountListFetcherTest { assertEitherRight(actual) coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWalletsSync + userWalletsListRepository.userWallets.value singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId1)) } } @@ -121,7 +124,7 @@ class DefaultMultiAccountListFetcherTest { // Arrange val params = MultiAccountListFetcher.Params.All - every { userWalletsStore.userWalletsSync } returns userWallets + every { userWalletsListRepository.userWallets.value } returns userWallets val exception = Exception("Fetch failed") coEvery { @@ -145,7 +148,7 @@ class DefaultMultiAccountListFetcherTest { assertEitherLeft(actual, expected) coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWalletsSync + userWalletsListRepository.userWallets.value singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId1)) singleAccountListFetcher(params = SingleAccountListFetcher.Params(userWalletId2)) } diff --git a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt index 8f3c2695b2..d080bf325f 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt @@ -1,8 +1,8 @@ package com.tangem.data.account.producer import com.google.common.truth.Truth -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList +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 @@ -27,15 +27,15 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultMultiAccountListProducerTest { - private val userWalletsStore: UserWalletsStore = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk() private val walletAccountListFlowFactory: WalletAccountListFlowFactory = mockk() private val flowProducerTools: FlowProducerTools = mockk() private val producer = DefaultMultiAccountListProducer( params = Unit, - userWalletsStore = userWalletsStore, - walletAccountListFlowFactory = walletAccountListFlowFactory, flowProducerTools = flowProducerTools, + userWalletsListRepository = userWalletsListRepository, + walletAccountListFlowFactory = walletAccountListFlowFactory, dispatchers = TestingCoroutineDispatcherProvider(), ) @@ -46,14 +46,14 @@ class DefaultMultiAccountListProducerTest { @AfterEach fun tearDownEach() { - clearMocks(userWalletsStore, walletAccountListFlowFactory) + clearMocks(userWalletsListRepository, walletAccountListFlowFactory) } @Test fun produce() = runTest { // Arrange val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow + every { userWalletsListRepository.loadAndGet() } returns userWalletsFlow val accountList = AccountList.empty(userWalletId) every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList) @@ -66,7 +66,7 @@ class DefaultMultiAccountListProducerTest { Truth.assertThat(actual).containsExactly(expected) coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets + userWalletsListRepository.loadAndGet() walletAccountListFlowFactory.create(userWalletId) } } @@ -75,7 +75,7 @@ class DefaultMultiAccountListProducerTest { fun `flow will updated if factoryFlow is updated`() = runTest { // Arrange val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow + every { userWalletsListRepository.loadAndGet() } returns userWalletsFlow val accountList = AccountList.empty(userWalletId) val updatedAccountList = AccountList.empty(userWalletId = userWalletId, sortType = TokensSortType.NONE) @@ -98,9 +98,9 @@ class DefaultMultiAccountListProducerTest { Truth.assertThat(secondEmission).containsExactly(listOf(updatedAccountList)) coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets + userWalletsListRepository.loadAndGet() walletAccountListFlowFactory.create(userWalletId) - userWalletsStore.userWallets + userWalletsListRepository.loadAndGet() walletAccountListFlowFactory.create(userWalletId) } } @@ -109,7 +109,7 @@ class DefaultMultiAccountListProducerTest { fun `flow is filtered the same response`() = runTest { // Arrange val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow + every { userWalletsListRepository.loadAndGet() } returns userWalletsFlow val accountList = AccountList.empty(userWalletId) val factoryFlow = MutableStateFlow(null) @@ -131,9 +131,9 @@ class DefaultMultiAccountListProducerTest { Truth.assertThat(secondEmission).containsExactly(listOf(accountList)) coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets + userWalletsListRepository.loadAndGet() walletAccountListFlowFactory.create(userWalletId) - userWalletsStore.userWallets + userWalletsListRepository.loadAndGet() walletAccountListFlowFactory.create(userWalletId) } } @@ -143,7 +143,7 @@ class DefaultMultiAccountListProducerTest { fun `flow returns empty list if factory throws exception`() = runTest { // Arrange val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow + every { userWalletsListRepository.loadAndGet() } returns userWalletsFlow val exception = RuntimeException("Converter error") every { walletAccountListFlowFactory.create(userWalletId) } throws exception @@ -156,7 +156,7 @@ class DefaultMultiAccountListProducerTest { Truth.assertThat(actual).containsExactly(expected) coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets + userWalletsListRepository.loadAndGet() walletAccountListFlowFactory.create(userWalletId) } } @@ -165,7 +165,7 @@ class DefaultMultiAccountListProducerTest { fun `flow is empty if userWalletsFlow returns empty flow`() = runTest { // Arrange val userWalletsFlow = emptyFlow>() - every { userWalletsStore.userWallets } returns userWalletsFlow + every { userWalletsListRepository.loadAndGet() } returns userWalletsFlow // Act val actual = producer.produce().let(::getEmittedValues) @@ -173,7 +173,7 @@ class DefaultMultiAccountListProducerTest { // Assert Truth.assertThat(actual).isEmpty() // no emissions - coVerify(exactly = 1) { userWalletsStore.userWallets } + coVerify(exactly = 1) { userWalletsListRepository.loadAndGet() } coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) } } @@ -181,7 +181,7 @@ class DefaultMultiAccountListProducerTest { fun `flow is empty if factory returns empty flow`() = runTest { // Arrange val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow + every { userWalletsListRepository.loadAndGet() } returns userWalletsFlow every { walletAccountListFlowFactory.create(userWalletId) } returns emptyFlow() @@ -192,7 +192,7 @@ class DefaultMultiAccountListProducerTest { Truth.assertThat(actual).isEmpty() // no emissions coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets + userWalletsListRepository.loadAndGet() walletAccountListFlowFactory.create(userWalletId) } } @@ -206,7 +206,7 @@ class DefaultMultiAccountListProducerTest { } val userWalletsFlow = MutableStateFlow(listOf(userWallet, userWallet2)) - every { userWalletsStore.userWallets } returns userWalletsFlow + every { userWalletsListRepository.loadAndGet() } returns userWalletsFlow val accountList = AccountList.empty(userWalletId) every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList) @@ -219,7 +219,7 @@ class DefaultMultiAccountListProducerTest { Truth.assertThat(actual).isEmpty() // no emissions coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets + userWalletsListRepository.loadAndGet() walletAccountListFlowFactory.create(userWalletId) walletAccountListFlowFactory.create(userWalletId2) } diff --git a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducerTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducerTest.kt index daf026b88f..a923d82c5c 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducerTest.kt @@ -8,8 +8,8 @@ import com.tangem.common.test.domain.wallet.MockUserWalletFactory import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.configs.GenericCardConfig +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency @@ -35,23 +35,23 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() private val params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWallet.walletId) - private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true) + private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true) private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory = mockk() private val flowProducerTools: FlowProducerTools = mockk() private val producer = DefaultMultiWalletCryptoCurrenciesProducer( params = params, - userWalletsStore = userWalletsStore, + flowProducerTools = flowProducerTools, + userWalletsListRepository = userWalletsListRepository, userTokensResponseStore = userTokensResponseStore, responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, - flowProducerTools = flowProducerTools, dispatchers = TestingCoroutineDispatcherProvider(), ) @BeforeEach fun resetMocks() { - clearMocks(userWalletsStore, userTokensResponseStore, responseCryptoCurrenciesFactory) + clearMocks(userWalletsListRepository, userTokensResponseStore, responseCryptoCurrenciesFactory) } @Test @@ -59,7 +59,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { // Arrange val userTokensResponseFlow = flowOf(null) - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet + every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns userWallet every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow // Act @@ -72,7 +72,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual.first()).isEqualTo(expected) verifyOrder { - userWalletsStore.getSyncStrict(params.userWalletId) + userWalletsListRepository.getSyncStrict(params.userWalletId) userTokensResponseStore.get(params.userWalletId) } @@ -113,7 +113,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin), ) - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet + every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns userWallet every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow every { @@ -146,7 +146,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder { - userWalletsStore.getSyncStrict(params.userWalletId) + userWalletsListRepository.getSyncStrict(params.userWalletId) userTokensResponseStore.get(params.userWalletId) responseCryptoCurrenciesFactory.createCurrencies( response = userTokensResponse, @@ -188,7 +188,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { val cryptoCurrencies = emptySet() - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet + every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns userWallet every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow every { @@ -213,7 +213,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder { - userWalletsStore.getSyncStrict(params.userWalletId) + userWalletsListRepository.getSyncStrict(params.userWalletId) userTokensResponseStore.get(params.userWalletId) responseCryptoCurrenciesFactory.createCurrencies( response = userTokensResponse, @@ -257,7 +257,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { } .buffer(capacity = 5) - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet + every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns userWallet every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow every { @@ -279,7 +279,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder { - userWalletsStore.getSyncStrict(params.userWalletId) + userWalletsListRepository.getSyncStrict(params.userWalletId) userTokensResponseStore.get(params.userWalletId) } @@ -304,7 +304,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { @Test fun `flow is empty if store returns empty flow`() = runTest { // Arrange - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet + every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns userWallet every { userTokensResponseStore.get(params.userWalletId) } returns emptyFlow() // Act @@ -316,7 +316,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual.first()).isEqualTo(expected) verifyOrder { - userWalletsStore.getSyncStrict(params.userWalletId) + userWalletsListRepository.getSyncStrict(params.userWalletId) userTokensResponseStore.get(params.userWalletId) } @@ -332,7 +332,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { every { isMultiCurrency } returns false } - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet // Act val actual = runCatching { producer.produce() }.exceptionOrNull() @@ -345,7 +345,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual).isInstanceOf(expected::class.java) Truth.assertThat(actual).hasMessageThat().isEqualTo(expected.message) - verifyOrder { userWalletsStore.getSyncStrict(params.userWalletId) } + verifyOrder { userWalletsListRepository.getSyncStrict(params.userWalletId) } verify(inverse = true) { userTokensResponseStore.get(any()) diff --git a/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt index 47b5fb3992..61d285462f 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt @@ -9,8 +9,8 @@ import com.tangem.data.account.store.AccountsResponseStore import com.tangem.data.account.store.AccountsResponseStoreFactory import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency @@ -29,7 +29,7 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WalletAccountListFlowFactoryTest { - private val userWalletsStore: UserWalletsStore = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk() private val accountsResponseStoreFactory: AccountsResponseStoreFactory = mockk() private val accountsResponseStore: AccountsResponseStore = mockk() private val accountsResponseStoreFlow = MutableStateFlow(value = null) @@ -40,7 +40,7 @@ class WalletAccountListFlowFactoryTest { private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() private val factory = WalletAccountListFlowFactory( - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, accountsResponseStoreFactory = accountsResponseStoreFactory, accountListConverterFactory = accountListConverterFactory, cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, @@ -52,7 +52,7 @@ class WalletAccountListFlowFactoryTest { @AfterEach fun tearDownEach() { clearMocks( - userWalletsStore, + userWalletsListRepository, accountsResponseStoreFactory, accountsResponseStore, accountListConverterFactory, @@ -70,7 +70,7 @@ class WalletAccountListFlowFactoryTest { every { this@mockk.isMultiCurrency } returns true } - every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet + every { userWalletsListRepository.getSyncStrict(userWalletId) } returns userWallet val accountsResponse = createGetWalletAccountsResponse(userWalletId) every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore @@ -105,7 +105,7 @@ class WalletAccountListFlowFactoryTest { fun `create for single wallet`() = runTest { val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false) - every { userWalletsStore.getSyncStrict(userWallet.walletId) } returns userWallet + every { userWalletsListRepository.getSyncStrict(userWallet.walletId) } returns userWallet val currency = cryptoCurrencyFactory.ethereum every { cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet) } returns currency @@ -134,7 +134,7 @@ class WalletAccountListFlowFactoryTest { fun `flow is created for single wallet with token`() = runTest { val nodl = MockUserWalletFactory.createSingleWalletWithToken() - every { userWalletsStore.getSyncStrict(nodl.walletId) } returns nodl + every { userWalletsListRepository.getSyncStrict(nodl.walletId) } returns nodl val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() every { diff --git a/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt index 68741d16a3..0adcc20b4a 100644 --- a/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt @@ -17,7 +17,6 @@ import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResp import com.tangem.datasource.api.tangemTech.models.account.GetWalletArchivedAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.datasource.local.datastore.RuntimeStateStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.models.account.Account.CryptoPortfolio @@ -52,7 +51,6 @@ class DefaultAccountsCRUDRepositoryTest { private val archivedAccountsInnerStore = RuntimeStateStore?>(defaultValue = null) private val archivedAccountsStore = ArchivedAccountsStore(runtimeStore = archivedAccountsInnerStore) - private val userWalletsStore: UserWalletsStore = mockk() private val userTokensSaver: UserTokensSaver = mockk() private val archivedAccountsETagStore: RuntimeStateStore> = mockk(relaxUnitFun = true) @@ -67,7 +65,6 @@ class DefaultAccountsCRUDRepositoryTest { walletAccountsSaver = walletAccountsSaver, accountsResponseStoreFactory = accountsResponseStoreFactory, archivedAccountsStoreFactory = archivedAccountsStoreFactory, - userWalletsStore = userWalletsStore, userTokensSaver = userTokensSaver, archivedAccountsETagStore = archivedAccountsETagStore, convertersContainer = convertersContainer, diff --git a/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt index 86949ca186..6ed7920120 100644 --- a/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt @@ -8,8 +8,8 @@ import com.tangem.data.common.currency.UserTokensResponseFactory import com.tangem.data.common.network.NetworkFactory import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.account.Account import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -23,14 +23,14 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultWalletAccountsResponseFactoryTest { - private val userWalletsStore = mockk() + private val userWalletsListRepository = mockk() private val cryptoPortfolioCF = mockk() private val cryptoPortfolioConverter = mockk() private val userTokensResponseFactory = mockk() private val networkFactory = mockk() private val factory = DefaultWalletAccountsResponseFactory( - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, cryptoPortfolioCF = cryptoPortfolioCF, userTokensResponseFactory = userTokensResponseFactory, networkFactory = networkFactory, @@ -46,7 +46,7 @@ class DefaultWalletAccountsResponseFactoryTest { @AfterEach fun tearDownEach() { clearMocks( - userWalletsStore, + userWalletsListRepository, cryptoPortfolioCF, cryptoPortfolioConverter, userTokensResponseFactory, @@ -63,7 +63,7 @@ class DefaultWalletAccountsResponseFactoryTest { tokens = emptyList(), ) - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns null + coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns null every { userTokensResponseFactory.createDefaultResponse( userWallet = null, @@ -89,7 +89,7 @@ class DefaultWalletAccountsResponseFactoryTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - userWalletsStore.getSyncOrNull(userWalletId) + userWalletsListRepository.getSyncOrNull(userWalletId) userTokensResponseFactory.createDefaultResponse( userWallet = null, networkFactory = networkFactory, @@ -105,7 +105,7 @@ class DefaultWalletAccountsResponseFactoryTest { every { walletId } returns userWalletId } - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns userWallet val accounts = AccountList.empty(userWallet.walletId).accounts .filterIsInstance() @@ -145,7 +145,7 @@ class DefaultWalletAccountsResponseFactoryTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - userWalletsStore.getSyncOrNull(userWalletId) + userWalletsListRepository.getSyncOrNull(userWalletId) cryptoPortfolioConverter.convertListBack(accounts) userTokensResponseFactory.createDefaultResponse( userWallet = userWallet, @@ -165,7 +165,7 @@ class DefaultWalletAccountsResponseFactoryTest { val accounts = AccountList.empty(userWallet.walletId).accounts .filterIsInstance() - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns userWallet val defaultResponse = UserTokensResponse( group = UserTokensResponse.GroupType.NETWORK, @@ -206,7 +206,7 @@ class DefaultWalletAccountsResponseFactoryTest { val userWallet = mockk(relaxed = true) { every { walletId } returns userWalletId } - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns userWallet val userTokensResponse = UserTokensResponse( group = UserTokensResponse.GroupType.NETWORK, diff --git a/domain/account/build.gradle.kts b/domain/account/build.gradle.kts index 0d035a8fe5..c3434b09e3 100644 --- a/domain/account/build.gradle.kts +++ b/domain/account/build.gradle.kts @@ -10,6 +10,7 @@ tasks.withType().configureEach { dependencies { + api(projects.domain.common) api(projects.domain.core) api(projects.domain.models) api(projects.domain.wallets.models) diff --git a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt index 8967491701..59048e8fab 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt @@ -6,7 +6,6 @@ import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow @@ -108,20 +107,6 @@ interface AccountsCRUDRepository { */ fun getTotalActiveAccountsCount(userWalletId: UserWalletId): Flow> - /** - * Retrieves a user wallet by its unique identifier - * - * @param userWalletId the unique identifier of the user wallet - * @return the [UserWallet] associated with the given identifier - */ - fun getUserWallet(userWalletId: UserWalletId): UserWallet - - /** Provides a flow of all user wallets */ - fun getUserWallets(): Flow> - - /** Synchronously retrieves all user wallets */ - fun getUserWalletsSync(): List - /** Checks if the provided account name is the default name within the given account list * * @param accountList the list of accounts to check against diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt index 86aa4adc9c..f374e1867d 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Option import arrow.core.getOrElse import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -14,12 +15,15 @@ import kotlinx.coroutines.flow.* * Accounts mode is considered enabled if there are at least two accounts in any of the user wallets that support * multiple currencies. * - * @property crudRepository repository to interact with user wallets and their accounts + * @property crudRepository repository to perform CRUD operations on accounts. + * @property userWalletsListRepository repository to get the list of user wallets. + * @property accountsFeatureToggles feature toggles for accounts. * [REDACTED_AUTHOR] */ class IsAccountsModeEnabledUseCase( private val crudRepository: AccountsCRUDRepository, + private val userWalletsListRepository: UserWalletsListRepository, private val accountsFeatureToggles: AccountsFeatureToggles, ) { @@ -27,7 +31,7 @@ class IsAccountsModeEnabledUseCase( operator fun invoke(): Flow { if (!accountsFeatureToggles.isFeatureEnabled) return flowOf(value = false) - return crudRepository.getUserWallets() + return userWalletsListRepository.loadAndGet() .flatMapLatest { userWallets -> val totalAccountsCountList = getTotalAccountsCountList(userWallets) @@ -40,7 +44,7 @@ class IsAccountsModeEnabledUseCase( suspend fun invokeSync(): Boolean { if (!accountsFeatureToggles.isFeatureEnabled) return false - return crudRepository.getUserWalletsSync() + return userWalletsListRepository.userWallets.value.orEmpty() .map { userWallet -> // If the wallet does not support multiple currencies, we consider its account count as 0 if (!userWallet.isMultiCurrency) return@map 0 @@ -50,7 +54,6 @@ class IsAccountsModeEnabledUseCase( .isModeEnabled() } - @Suppress("UnusedFlow") private fun getTotalAccountsCountList(userWallets: List): List> { return userWallets .map { userWallet -> diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt index e5d8cfedc1..9b9f38a4c6 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt @@ -5,6 +5,7 @@ import arrow.core.some import com.google.common.truth.Truth import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency @@ -24,13 +25,18 @@ import org.junit.jupiter.api.TestInstance class IsAccountsModeEnabledUseCaseTest { private val accountsCRUDRepository: AccountsCRUDRepository = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk() private val featureToggles: AccountsFeatureToggles = mockk() - private val useCase = IsAccountsModeEnabledUseCase(accountsCRUDRepository, featureToggles) + private val useCase = IsAccountsModeEnabledUseCase( + crudRepository = accountsCRUDRepository, + userWalletsListRepository = userWalletsListRepository, + accountsFeatureToggles = featureToggles, + ) @AfterEach fun tearDown() { - clearMocks(accountsCRUDRepository, featureToggles) + clearMocks(userWalletsListRepository, accountsCRUDRepository, featureToggles) } @Nested @@ -49,14 +55,14 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isFalse() verify(exactly = 1) { featureToggles.isFeatureEnabled } - verify(inverse = true) { accountsCRUDRepository.getUserWallets() } + verify(inverse = true) { userWalletsListRepository.loadAndGet() } } @Test - fun `returns false when getUserWallets emits empty flow`() = runTest { + fun `returns false when loadAndGet emits empty flow`() = runTest { // Arrange every { featureToggles.isFeatureEnabled } returns true - every { accountsCRUDRepository.getUserWallets() } returns emptyFlow() + every { userWalletsListRepository.loadAndGet() } returns emptyFlow() // Act val actual = useCase.invoke().firstOrNull() @@ -66,19 +72,19 @@ class IsAccountsModeEnabledUseCaseTest { verifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWallets() + userWalletsListRepository.loadAndGet() } verify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCount(any()) } } @Test - fun `returns false when getUserWallets emits one wallet with isMultiCurrency false`() = runTest { + fun `returns false when loadAndGet emits one wallet with isMultiCurrency false`() = runTest { // Arrange val wallet = createUserWallet(isMultiCurrency = false) every { featureToggles.isFeatureEnabled } returns true - every { accountsCRUDRepository.getUserWallets() } returns flowOf(listOf(wallet)) + every { userWalletsListRepository.loadAndGet() } returns flowOf(listOf(wallet)) // Act val actual = useCase.invoke().first() @@ -88,19 +94,19 @@ class IsAccountsModeEnabledUseCaseTest { verifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWallets() + userWalletsListRepository.loadAndGet() } verify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCount(any()) } } @Test - fun `returns true when getUserWallets emits one wallet with isMultiCurrency true`() = runTest { + fun `returns true when loadAndGet emits one wallet with isMultiCurrency true`() = runTest { // Arrange val wallet = createUserWallet(isMultiCurrency = true) every { featureToggles.isFeatureEnabled } returns true - every { accountsCRUDRepository.getUserWallets() } returns flowOf(listOf(wallet)) + every { userWalletsListRepository.loadAndGet() } returns flowOf(listOf(wallet)) every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } returns flowOf(2.some()) // Act @@ -111,18 +117,18 @@ class IsAccountsModeEnabledUseCaseTest { verifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWallets() + userWalletsListRepository.loadAndGet() accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } } @Test - fun `returns false when getUserWallets emits one wallet with isMultiCurrency true and None counts`() = runTest { + fun `returns false when loadAndGet emits one wallet with isMultiCurrency true and None counts`() = runTest { // Arrange val wallet = createUserWallet(isMultiCurrency = true) every { featureToggles.isFeatureEnabled } returns true - every { accountsCRUDRepository.getUserWallets() } returns flowOf(listOf(wallet)) + every { userWalletsListRepository.loadAndGet() } returns flowOf(listOf(wallet)) every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } returns flowOf(none()) // Act @@ -133,19 +139,19 @@ class IsAccountsModeEnabledUseCaseTest { verifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWallets() + userWalletsListRepository.loadAndGet() accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } } @Test - fun `returns true when getUserWallets emits two wallets, one isMultiCurrency false, one true`() = runTest { + fun `returns true when loadAndGet emits two wallets, one isMultiCurrency false, one true`() = runTest { // Arrange val wallet1 = createUserWallet(isMultiCurrency = false) val wallet2 = createUserWallet(isMultiCurrency = true) every { featureToggles.isFeatureEnabled } returns true - every { accountsCRUDRepository.getUserWallets() } returns flowOf(listOf(wallet1, wallet2)) + every { userWalletsListRepository.loadAndGet() } returns flowOf(listOf(wallet1, wallet2)) every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet2.walletId) } returns flowOf(2.some()) // Act @@ -156,7 +162,7 @@ class IsAccountsModeEnabledUseCaseTest { verifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWallets() + userWalletsListRepository.loadAndGet() accountsCRUDRepository.getTotalActiveAccountsCount(wallet2.walletId) } @@ -180,14 +186,14 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isFalse() verify(exactly = 1) { featureToggles.isFeatureEnabled } - verify(inverse = true) { accountsCRUDRepository.getUserWalletsSync() } + verify(inverse = true) { userWalletsListRepository.userWallets.value } } @Test fun `returns false when getUserWalletsSync returns empty list`() = runTest { // Arrange every { featureToggles.isFeatureEnabled } returns true - every { accountsCRUDRepository.getUserWalletsSync() } returns emptyList() + every { userWalletsListRepository.userWallets.value } returns emptyList() // Act val actual = useCase.invokeSync() @@ -197,7 +203,7 @@ class IsAccountsModeEnabledUseCaseTest { verifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value } coVerify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCountSync(any()) } @@ -209,7 +215,7 @@ class IsAccountsModeEnabledUseCaseTest { val wallet = createUserWallet(isMultiCurrency = false) every { featureToggles.isFeatureEnabled } returns true - every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(wallet) + every { userWalletsListRepository.userWallets.value } returns listOf(wallet) // Act val actual = useCase.invokeSync() @@ -219,7 +225,7 @@ class IsAccountsModeEnabledUseCaseTest { verifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value } coVerify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCountSync(any()) } @@ -231,7 +237,7 @@ class IsAccountsModeEnabledUseCaseTest { val wallet = createUserWallet(isMultiCurrency = true) every { featureToggles.isFeatureEnabled } returns true - every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(wallet) + every { userWalletsListRepository.userWallets.value } returns listOf(wallet) coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } returns 2.some() // Act @@ -242,7 +248,7 @@ class IsAccountsModeEnabledUseCaseTest { coVerifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } } @@ -253,7 +259,7 @@ class IsAccountsModeEnabledUseCaseTest { val wallet = createUserWallet(isMultiCurrency = true) every { featureToggles.isFeatureEnabled } returns true - every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(wallet) + every { userWalletsListRepository.userWallets.value } returns listOf(wallet) coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } returns none() // Act @@ -264,7 +270,7 @@ class IsAccountsModeEnabledUseCaseTest { coVerifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } } @@ -276,7 +282,7 @@ class IsAccountsModeEnabledUseCaseTest { val wallet2 = createUserWallet(isMultiCurrency = true) every { featureToggles.isFeatureEnabled } returns true - every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(wallet1, wallet2) + every { userWalletsListRepository.userWallets.value } returns listOf(wallet1, wallet2) coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet2.walletId) } returns 2.some() // Act @@ -287,7 +293,7 @@ class IsAccountsModeEnabledUseCaseTest { coVerifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet2.walletId) } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt index fced65957e..a228aceb2e 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt @@ -7,6 +7,7 @@ import com.tangem.domain.account.status.usecase.* import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher import com.tangem.domain.account.status.utils.CryptoCurrencyMetadataCleaner import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier @@ -36,12 +37,12 @@ internal object AccountStatusUseCaseModule { @Provides @Singleton fun provideGetAccountCurrencyByAddressUseCase( - accountsCRUDRepository: AccountsCRUDRepository, + userWalletsListRepository: UserWalletsListRepository, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, singleAccountListSupplier: SingleAccountListSupplier, ): GetAccountCurrencyByAddressUseCase { return GetAccountCurrencyByAddressUseCase( - accountsCRUDRepository = accountsCRUDRepository, + userWalletsListRepository = userWalletsListRepository, multiNetworkStatusSupplier = multiNetworkStatusSupplier, singleAccountListSupplier = singleAccountListSupplier, ) @@ -50,12 +51,12 @@ internal object AccountStatusUseCaseModule { @Provides @Singleton fun provideGetCryptoCurrencyActionsUseCaseV2( - accountsCRUDRepository: AccountsCRUDRepository, + userWalletsListRepository: UserWalletsListRepository, singleAccountStatusListSupplier: SingleAccountStatusListSupplier, getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, ): GetCryptoCurrencyActionsUseCaseV2 { return GetCryptoCurrencyActionsUseCaseV2( - accountsCRUDRepository = accountsCRUDRepository, + userWalletsListRepository = userWalletsListRepository, singleAccountStatusListSupplier = singleAccountStatusListSupplier, getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase, ) diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducer.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducer.kt index 13910406f9..e5c1ecf871 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducer.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducer.kt @@ -3,8 +3,8 @@ package com.tangem.domain.account.status.producer import arrow.core.Option import arrow.core.some import com.tangem.domain.account.models.AccountStatusList -import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted @@ -20,7 +20,8 @@ import kotlinx.coroutines.flow.flowOn * Produces a flow of [AccountStatusList] for multiple user wallets. * * @property params Parameters for the producer (currently unused). - * @property accountsCRUDRepository Repository to get the list of user wallets. + * @property flowProducerTools Tools for managing the flow producer. + * @property userWalletsListRepository Repository to get the list of user wallets. * @property singleAccountStatusListSupplier Supplier to get the account status list for a single user wallet. * @property dispatchers Coroutine dispatcher provider for managing threading. * @@ -29,7 +30,7 @@ import kotlinx.coroutines.flow.flowOn internal class DefaultMultiAccountStatusListProducer @AssistedInject constructor( @Assisted val params: Unit, override val flowProducerTools: FlowProducerTools, - private val accountsCRUDRepository: AccountsCRUDRepository, + private val userWalletsListRepository: UserWalletsListRepository, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val dispatchers: CoroutineDispatcherProvider, ) : MultiAccountStatusListProducer { @@ -38,7 +39,7 @@ internal class DefaultMultiAccountStatusListProducer @AssistedInject constructor @OptIn(ExperimentalCoroutinesApi::class) override fun produce(): Flow> { - return accountsCRUDRepository.getUserWallets() + return userWalletsListRepository.loadAndGet() .flatMapLatest { userWallets -> val flows = userWallets.map { singleAccountStatusListSupplier( 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 5b5381ffa6..956cc9e4ce 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 @@ -7,8 +7,8 @@ import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.domain.account.models.AccountCurrencyId import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.AccountStatusList -import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceLoading @@ -55,10 +55,16 @@ import java.math.BigDecimal * Produces a flow of [AccountStatusList] for a single user wallet. * * @property params Parameters containing the user wallet ID. - * @property accountsCRUDRepository Repository for accessing account data. + * @property flowProducerTools Tools for managing the flow producer. + * @property userWalletsListRepository Repository for getting user wallet by id. * @property singleAccountListSupplier Supplier to get the list of accounts for the user wallet. - * @property cryptoCurrencyStatusesFlowFactory Factory to create flows of cryptocurrency statuses. + * @property networksRepository Repository for checking network statuses in a cache. * @property dispatchers Coroutine dispatcher provider for managing threading. + * @property networkStatusSupplier Supplier for getting network statuses. + * @property quoteStatusSupplier Supplier for getting quote statuses. + * @property stakingBalanceSupplier Supplier for getting staking balances. + * @property stakingIdFactory Factory for creating staking IDs. + * @property analyticsExceptionHandler Handler for analytics exceptions. * [REDACTED_AUTHOR] */ @@ -66,11 +72,11 @@ import java.math.BigDecimal @OptIn(ExperimentalCoroutinesApi::class) internal class DefaultSingleAccountStatusListProducer @AssistedInject constructor( @Assisted private val params: SingleAccountStatusListProducer.Params, - private val accountsCRUDRepository: AccountsCRUDRepository, + override val flowProducerTools: FlowProducerTools, + private val userWalletsListRepository: UserWalletsListRepository, private val singleAccountListSupplier: SingleAccountListSupplier, private val networksRepository: NetworksRepository, private val dispatchers: CoroutineDispatcherProvider, - override val flowProducerTools: FlowProducerTools, private val networkStatusSupplier: MultiNetworkStatusSupplier, private val quoteStatusSupplier: MultiQuoteStatusSupplier, private val stakingBalanceSupplier: MultiStakingBalanceSupplier, @@ -88,7 +94,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo @Suppress("LongMethod") private fun flattenFlow(): Flow = channelFlow { val walletId = params.userWalletId - val userWallet = accountsCRUDRepository.getUserWallet(userWalletId = params.userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) val flattenCurrency: MutableSharedFlow> = MutableSharedFlow( replay = 1, 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 31de28f37e..71bf409b07 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 @@ -9,9 +9,9 @@ import arrow.core.raise.option import arrow.core.toNonEmptyListOrNull import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.SingleAccountListProducer -import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.status.model.AccountCryptoCurrency import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.account.Account import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.getAddress @@ -29,14 +29,14 @@ private typealias WalletIdWithNetworkId = Pair /** * Use case to retrieve an [AccountCryptoCurrency] based on a provided address. * - * @property accountsCRUDRepository Repository to access user wallets. + * @property userWalletsListRepository Repository to access user wallets. * @property multiNetworkStatusSupplier Supplier to get network status for multiple networks. * @property singleAccountListSupplier Supplier to get account lists for a single wallet. * [REDACTED_AUTHOR] */ class GetAccountCurrencyByAddressUseCase( - private val accountsCRUDRepository: AccountsCRUDRepository, + private val userWalletsListRepository: UserWalletsListRepository, private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val singleAccountListSupplier: SingleAccountListSupplier, ) { @@ -65,7 +65,7 @@ class GetAccountCurrencyByAddressUseCase( } private fun OptionRaise.getUserWalletIds(): NonEmptyList { - val userWalletIds = accountsCRUDRepository.getUserWalletsSync() + val userWalletIds = userWalletsListRepository.userWallets.value.orEmpty() .filter(UserWallet::isMultiCurrency) .map(UserWallet::walletId) .toNonEmptyListOrNull() diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetCryptoCurrencyActionsUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetCryptoCurrencyActionsUseCaseV2.kt index d9185beaf3..9ec1d14e63 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetCryptoCurrencyActionsUseCaseV2.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetCryptoCurrencyActionsUseCaseV2.kt @@ -1,9 +1,9 @@ package com.tangem.domain.account.status.usecase -import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.utils.AccountCryptoCurrencyStatusFinder +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase @@ -17,14 +17,14 @@ import kotlinx.coroutines.flow.transformLatest /** * Use case to retrieve the available actions for a specific cryptocurrency associated with an account. * - * @property accountsCRUDRepository repository for account CRUD operations. + * @property userWalletsListRepository repository to get the user wallets list. * @property singleAccountStatusListSupplier supplier to get the list of account statuses. * @property getCryptoCurrencyActionsUseCase use case to get the actions for a specific cryptocurrency status. * [REDACTED_AUTHOR] */ class GetCryptoCurrencyActionsUseCaseV2( - private val accountsCRUDRepository: AccountsCRUDRepository, + private val userWalletsListRepository: UserWalletsListRepository, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, ) { @@ -45,7 +45,7 @@ class GetCryptoCurrencyActionsUseCaseV2( ) if (accountCurrencyStatus != null) { - val userWallet = accountsCRUDRepository.getUserWallet(userWalletId = accountId.userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = accountId.userWalletId) val actionsFlow = getCryptoCurrencyActionsUseCase( userWallet = userWallet, cryptoCurrencyStatus = accountCurrencyStatus.status, diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt index 2b32c8397b..48ecd270a8 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt @@ -2,8 +2,8 @@ package com.tangem.domain.account.status.producer import com.google.common.truth.Truth import com.tangem.domain.account.models.AccountStatusList -import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -21,7 +21,7 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultMultiAccountStatusListProducerTest { - private val accountsCRUDRepository: AccountsCRUDRepository = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk() private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier = mockk() private val dispatchers = TestingCoroutineDispatcherProvider() private val flowProducerTools: FlowProducerTools = mockk() @@ -38,7 +38,7 @@ class DefaultMultiAccountStatusListProducerTest { private val producer = DefaultMultiAccountStatusListProducer( params = Unit, - accountsCRUDRepository = accountsCRUDRepository, + userWalletsListRepository = userWalletsListRepository, singleAccountStatusListSupplier = singleAccountStatusListSupplier, dispatchers = dispatchers, flowProducerTools = flowProducerTools, @@ -46,7 +46,7 @@ class DefaultMultiAccountStatusListProducerTest { @AfterEach fun tearDown() { - clearMocks(accountsCRUDRepository, singleAccountStatusListSupplier) + clearMocks(userWalletsListRepository, singleAccountStatusListSupplier) } @Test @@ -55,7 +55,7 @@ class DefaultMultiAccountStatusListProducerTest { val wallets = listOf(userWallet1, userWallet2) val walletsFlow = MutableStateFlow(wallets) - every { accountsCRUDRepository.getUserWallets() } returns walletsFlow + every { userWalletsListRepository.loadAndGet() } returns walletsFlow val accountStatusList1 = mockk() val accountStatusList2 = mockk() @@ -79,7 +79,7 @@ class DefaultMultiAccountStatusListProducerTest { Truth.assertThat(actual).containsExactly(expected) coVerify(ordering = Ordering.SEQUENCE) { - accountsCRUDRepository.getUserWallets() + userWalletsListRepository.loadAndGet() singleAccountStatusListSupplier( params = SingleAccountStatusListProducer.Params(userWalletId1), ) @@ -93,7 +93,7 @@ class DefaultMultiAccountStatusListProducerTest { fun `produce returns empty flow if userWallets is empty list`() = runTest { // Arrange val walletsFlow = MutableStateFlow>(emptyList()) - every { accountsCRUDRepository.getUserWallets() } returns walletsFlow + every { userWalletsListRepository.loadAndGet() } returns walletsFlow // Act val actual = producer.produce().let(::getEmittedValues) @@ -102,27 +102,26 @@ class DefaultMultiAccountStatusListProducerTest { Truth.assertThat(actual).isEmpty() coVerify(ordering = Ordering.SEQUENCE) { - accountsCRUDRepository.getUserWallets() + userWalletsListRepository.loadAndGet() } } - // TODO: uncomment after migration on UserWalletsListRepository - // @Test - // fun `produce returns empty flow if userWallets is null`() = runTest { - // // Arrange - // val walletsFlow = MutableStateFlow?>(null) - // every { accountsCRUDRepository.getUserWallets() } returns walletsFlow - // - // // Act - // val actual = producer.produce().let(::getEmittedValues) - // - // // Assert - // Truth.assertThat(actual).isEmpty() - // - // coVerify(ordering = Ordering.SEQUENCE) { - // accountsCRUDRepository.getUserWallets() - // } - // } + @Test + fun `produce returns empty flow if userWallets is null`() = runTest { + // Arrange + val walletsFlow = flowOf>(emptyList()) + every { userWalletsListRepository.loadAndGet() } returns walletsFlow + + // Act + val actual = producer.produce().let(::getEmittedValues) + + // Assert + Truth.assertThat(actual).isEmpty() + + coVerify(ordering = Ordering.SEQUENCE) { + userWalletsListRepository.loadAndGet() + } + } @Test fun `flow will updated if userWallets are updated`() = runTest { @@ -131,7 +130,7 @@ class DefaultMultiAccountStatusListProducerTest { val userWallet3 = mockk { every { walletId } returns userWalletId3 } val walletsFlow = MutableStateFlow(listOf(userWallet1, userWallet2)) - every { accountsCRUDRepository.getUserWallets() } returns walletsFlow + every { userWalletsListRepository.loadAndGet() } returns walletsFlow val accountStatusList1 = mockk() val accountStatusList2 = mockk() @@ -171,7 +170,7 @@ class DefaultMultiAccountStatusListProducerTest { Truth.assertThat(actual2).containsExactly(expected2) coVerify(ordering = Ordering.SEQUENCE) { - accountsCRUDRepository.getUserWallets() + userWalletsListRepository.loadAndGet() singleAccountStatusListSupplier( params = SingleAccountStatusListProducer.Params(userWalletId1), ) @@ -187,7 +186,7 @@ class DefaultMultiAccountStatusListProducerTest { singleAccountStatusListSupplier( params = SingleAccountStatusListProducer.Params(userWalletId3), ) - accountsCRUDRepository.getUserWallets() + userWalletsListRepository.loadAndGet() singleAccountStatusListSupplier( params = SingleAccountStatusListProducer.Params(userWalletId1), ) diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt index ba62fe222b..596ebd9242 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt @@ -3,9 +3,9 @@ package com.tangem.domain.account.status.usecase import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.SingleAccountListProducer -import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.status.model.AccountCryptoCurrency import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.network.NetworkStatus @@ -28,19 +28,19 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class GetAccountCurrencyByAddressUseCaseTest { - private val accountsCRUDRepository: AccountsCRUDRepository = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk() private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier = mockk() private val singleAccountListSupplier: SingleAccountListSupplier = mockk() private val useCase = GetAccountCurrencyByAddressUseCase( - accountsCRUDRepository = accountsCRUDRepository, + userWalletsListRepository = userWalletsListRepository, multiNetworkStatusSupplier = multiNetworkStatusSupplier, singleAccountListSupplier = singleAccountListSupplier, ) @AfterEach fun tearDown() { - clearMocks(accountsCRUDRepository, multiNetworkStatusSupplier, singleAccountListSupplier) + clearMocks(userWalletsListRepository, multiNetworkStatusSupplier, singleAccountListSupplier) } @Test @@ -55,27 +55,10 @@ class GetAccountCurrencyByAddressUseCaseTest { assertNone(actual) } - // TODO: uncomment after migration on UserWalletsListRepository - // @Test - // fun `returns None if userWalletIds is null`() = runTest { - // // Arrange - // every { accountsCRUDRepository.getUserWalletsSync() } returns MutableStateFlow(null) - // - // // Act - // val actual = useCase(validAddress) - // - // // Assert - // assertNone(actual) - // - // coVerifySequence { - // accountsCRUDRepository.getUserWalletsSync() - // } - // } - @Test - fun `returns None if userWalletIds is empty list`() = runTest { + fun `returns None if userWalletIds is null`() = runTest { // Arrange - every { accountsCRUDRepository.getUserWalletsSync() } returns emptyList() + every { userWalletsListRepository.userWallets.value } returns null // Act val actual = useCase(validAddress) @@ -84,7 +67,23 @@ class GetAccountCurrencyByAddressUseCaseTest { assertNone(actual) coVerifySequence { - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value + } + } + + @Test + fun `returns None if userWalletIds is empty list`() = runTest { + // Arrange + every { userWalletsListRepository.userWallets.value } returns emptyList() + + // Act + val actual = useCase(validAddress) + + // Assert + assertNone(actual) + + coVerifySequence { + userWalletsListRepository.userWallets.value } } @@ -95,7 +94,7 @@ class GetAccountCurrencyByAddressUseCaseTest { every { isMultiCurrency } returns false } - every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(singleWallet) + every { userWalletsListRepository.userWallets.value } returns listOf(singleWallet) // Act val actual = useCase(validAddress) @@ -104,14 +103,14 @@ class GetAccountCurrencyByAddressUseCaseTest { assertNone(actual) coVerifySequence { - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value } } @Test fun `returns None if multiNetworkStatusSupplier returns null`() = runTest { // Arrange - every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) + every { userWalletsListRepository.userWallets.value } returns listOf(multiUserWallet) coEvery { multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } returns null @@ -123,7 +122,7 @@ class GetAccountCurrencyByAddressUseCaseTest { assertNone(actual) coVerifySequence { - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } } @@ -131,7 +130,7 @@ class GetAccountCurrencyByAddressUseCaseTest { @Test fun `returns None if multiNetworkStatusSupplier returns empty list`() = runTest { // Arrange - every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) + every { userWalletsListRepository.userWallets.value } returns listOf(multiUserWallet) coEvery { multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } returns emptySet() @@ -143,7 +142,7 @@ class GetAccountCurrencyByAddressUseCaseTest { assertNone(actual) coVerifySequence { - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } } @@ -156,7 +155,7 @@ class GetAccountCurrencyByAddressUseCaseTest { value = NetworkStatus.Unreachable(address = null), ) - every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) + every { userWalletsListRepository.userWallets.value } returns listOf(multiUserWallet) coEvery { multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } returns setOf(networkStatus) @@ -168,7 +167,7 @@ class GetAccountCurrencyByAddressUseCaseTest { assertNone(actual) coVerifySequence { - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } } @@ -183,7 +182,7 @@ class GetAccountCurrencyByAddressUseCaseTest { value = NetworkStatus.Unreachable(address = validNetworkAddress), ) - every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) + every { userWalletsListRepository.userWallets.value } returns listOf(multiUserWallet) coEvery { multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } returns setOf(networkStatus) @@ -200,7 +199,7 @@ class GetAccountCurrencyByAddressUseCaseTest { assertNone(actual) coVerifySequence { - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) singleAccountListSupplier.getSyncOrNull( params = SingleAccountListProducer.Params(userWalletId = userWalletId), @@ -220,7 +219,7 @@ class GetAccountCurrencyByAddressUseCaseTest { ) val accountList = AccountList.empty(userWalletId) - every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) + every { userWalletsListRepository.userWallets.value } returns listOf(multiUserWallet) coEvery { multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } returns setOf(networkStatus) @@ -237,7 +236,7 @@ class GetAccountCurrencyByAddressUseCaseTest { assertNone(actual) coVerifySequence { - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) singleAccountListSupplier.getSyncOrNull( params = SingleAccountListProducer.Params(userWalletId = userWalletId), @@ -255,7 +254,7 @@ class GetAccountCurrencyByAddressUseCaseTest { ) val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = setOf(currency)) - every { accountsCRUDRepository.getUserWalletsSync() } returns listOf(multiUserWallet) + every { userWalletsListRepository.userWallets.value } returns listOf(multiUserWallet) coEvery { multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) } returns setOf(networkStatus) @@ -273,7 +272,7 @@ class GetAccountCurrencyByAddressUseCaseTest { assertSome(actual, expected) coVerifySequence { - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value multiNetworkStatusSupplier.getSyncOrNull(params = MultiNetworkStatusProducer.Params(userWalletId), 1000) singleAccountListSupplier.getSyncOrNull( params = SingleAccountListProducer.Params(userWalletId = userWalletId), diff --git a/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt index c9f9aecb00..fa1a12c054 100644 --- a/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt +++ b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt @@ -5,6 +5,7 @@ import com.tangem.domain.common.wallets.error.* import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow /** @@ -30,12 +31,21 @@ interface UserWalletsListRepository { */ val selectedUserWallet: StateFlow + /** Get user wallet by [id] */ + fun getSyncOrNull(id: UserWalletId): UserWallet? + + /** Get user wallet by [id] */ + fun getSyncStrict(id: UserWalletId): UserWallet + /** * Loads user wallets list and selected wallet. * If the list is already loaded, it does nothing. */ suspend fun load() + /** Loads user wallets list and selected wallet and returns a flow of the list */ + fun loadAndGet(): Flow> + /** * Gets and if necessary loads user wallets list and selected wallet. */ From d8e742975f43690c4f3b9bfb68b30395a3023b46 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Feb 2026 01:51:03 +0000 Subject: [PATCH 028/106] 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 4372af7f4b..1cc9fdda26 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.33-1423" +tangemBlockchainSdk = "develop-1419" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.33-576" +tangemCardSdk = "develop-577" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-545" +tangemHotSdk = "develop-539" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From f6d61b2b7f25be6365fbb62d662bcf56fd03c0c9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Feb 2026 15:40:17 +0400 Subject: [PATCH 029/106] Updated on 2026-08-14 --- .../choosetoken/model/ChooseManagedTokensModel.kt | 14 +++++++++++++- .../walletsettings/model/WalletSettingsModel.kt | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) 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 3698d18280..7c1d1b2f84 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 @@ -17,6 +17,8 @@ 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.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.models.account.AccountId import com.tangem.domain.notifications.SetShouldShowNotificationUseCase import com.tangem.features.managetokens.choosetoken.entity.ChooseManageTokensBottomSheetConfig import com.tangem.features.managetokens.choosetoken.entity.ChooseManagedTokenUM @@ -43,6 +45,7 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject +import kotlin.collections.isNotEmpty @Suppress("LongParameterList") @ModelScoped @@ -52,14 +55,23 @@ internal class ChooseManagedTokensModel @Inject constructor( private val uiMessageSender: UiMessageSender, private val setShouldShowNotificationUseCase: SetShouldShowNotificationUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + accountsFeatureToggles: AccountsFeatureToggles, paramsContainer: ParamsContainer, manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory, manageTokensListManagerFactory: ManageTokensListManager.Factory, ) : Model() { private val params: ChooseManagedTokensComponent.Params = paramsContainer.require() + + private val manageTokensMode = if (accountsFeatureToggles.isFeatureEnabled) { + val accountId = AccountId.forMainCryptoPortfolio(userWalletId = params.userWalletId) + ManageTokensMode.Account(accountId = accountId) + } else { + ManageTokensMode.Wallet(params.userWalletId) + } + private val useCasesFacade: ManageTokensUseCasesFacade = manageTokensUseCasesFacadeFactory - .create(mode = ManageTokensMode.Wallet(params.userWalletId)) + .create(mode = manageTokensMode) private val manageTokensListManager = manageTokensListManagerFactory.create( scope = modelScope, 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 a403130ffa..796d045499 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 @@ -237,7 +237,7 @@ internal class WalletSettingsModel @Inject constructor( router.push( AppRoute.ManageTokens( source = Source.SETTINGS, - portfolioId = if (isAccountsFeatureEnabled) { + portfolioId = if (accountsFeatureToggles.isFeatureEnabled) { PortfolioId( accountId = AccountId.forMainCryptoPortfolio(userWalletId = userWallet.walletId), ) From 8adc179449a5662018bd84dc7d2e986fd52b7682 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Feb 2026 13:29:55 +0400 Subject: [PATCH 030/106] Updated on 2026-08-14 --- data/express/build.gradle.kts | 1 + .../express/DefaultExpressServiceFetcher.kt | 6 +- data/nft/build.gradle.kts | 11 +- data/nft/detekt-baseline-debug.xml | 25 ---- .../tangem/data/nft/DefaultNFTRepository.kt | 120 +++++++++--------- .../com/tangem/data/nft/NFTCleanerTest.kt | 2 +- .../DefaultMultiStakingBalanceFetcher.kt | 22 ++-- .../DefaultMultiStakingBalanceFetcherTest.kt | 44 +++---- data/txhistory/build.gradle.kts | 5 +- data/txhistory/detekt-baseline-debug.xml | 4 - .../data/txhistory/di/TxHistoryDataModule.kt | 14 +- .../repository/DefaultTxHistoryRepository.kt | 27 ++-- .../DefaultWalletConnectRepository.kt | 15 +-- .../di/WalletConnectDataModule.kt | 7 +- .../DefaultWalletManagersFacade.kt | 6 +- .../referral/data/ReferralRepositoryImpl.kt | 14 +- .../referral/di/ReferralRepositoryModule.kt | 18 +-- .../feature/swap/DefaultSwapRepository.kt | 8 +- .../tangem/feature/swap/di/SwapDataModule.kt | 6 +- .../feature/swap/domain/SwapInteractor.kt | 14 +- .../feature/swap/domain/SwapInteractorImpl.kt | 9 +- .../feature/swap/domain/api/SwapRepository.kt | 9 +- 22 files changed, 175 insertions(+), 212 deletions(-) delete mode 100644 data/nft/detekt-baseline-debug.xml diff --git a/data/express/build.gradle.kts b/data/express/build.gradle.kts index 8051766229..f75d1dde55 100644 --- a/data/express/build.gradle.kts +++ b/data/express/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(projects.data.common) /** Domain */ + implementation(projects.domain.common) implementation(projects.domain.express.models) implementation(projects.domain.express) implementation(projects.domain.wallets.models) 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 e794c648b0..cb16c2ef93 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 @@ -10,7 +10,7 @@ import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.exchangeservice.swap.ExpressUtils.getRefCode import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.ExpressAssetsStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.catchOn import com.tangem.domain.core.utils.lceContent @@ -42,7 +42,7 @@ internal class DefaultExpressServiceFetcher @Inject constructor( private val tangemExpressApi: TangemExpressApi, private val expressAssetsStore: ExpressAssetsStore, private val appPreferencesStore: AppPreferencesStore, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val dispatchers: CoroutineDispatcherProvider, ) : ExpressServiceFetcher { @@ -52,7 +52,7 @@ internal class DefaultExpressServiceFetcher @Inject constructor( override suspend fun fetch(userWalletId: UserWalletId, assetIds: Set): Either = either { val userWallet = arrow.core.raise.catch( - block = { userWalletsStore.getSyncStrict(userWalletId) }, + block = { userWalletsListRepository.getSyncStrict(userWalletId) }, catch = ::raise, ) diff --git a/data/nft/build.gradle.kts b/data/nft/build.gradle.kts index 4124798b29..f53acc71ce 100644 --- a/data/nft/build.gradle.kts +++ b/data/nft/build.gradle.kts @@ -23,13 +23,14 @@ dependencies { implementation(projects.data.common) /** Project - Domain */ - implementation(projects.domain.models) - implementation(projects.domain.wallets.models) - implementation(projects.domain.tokens.models) - implementation(projects.domain.nft) - implementation(projects.domain.walletManager) implementation(projects.domain.card) + implementation(projects.domain.common) + implementation(projects.domain.models) + implementation(projects.domain.nft) implementation(projects.domain.nft.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.walletManager) + implementation(projects.domain.wallets.models) /** Project - Utils */ implementation(projects.core.utils) diff --git a/data/nft/detekt-baseline-debug.xml b/data/nft/detekt-baseline-debug.xml deleted file mode 100644 index d4e81aea8d..0000000000 --- a/data/nft/detekt-baseline-debug.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ NFTCollections( network = network, content = NFTCollections.Content.Collections( collections = it ?.map { collection -> nftSdkCollectionConverter.convert(network to collection) } ?.filter { it.id !is NFTCollection.Identifier.Unknown }, source = StatusSource.CACHE, ), ) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ if (it !is UnsupportedOperationException) { saveFailedStateInRuntime( userWalletId = userWalletId, network = network, error = it, ) } } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ if (it.id == collectionId) { it.changeAssetsStatusSource(source) } else { it } } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ if (it.identifier == sdkCollectionId) { it.copy(assets = assets) } else { it } } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ networkFactory.create( blockchain = it, extraDerivationPath = null, userWallet = userWallet, ) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ nftRuntimeStores[storeId] = it val storedCollections = getStoredCollections(userWalletId, network) val storedPrices = getStoredPrices(userWalletId, network) it.initialize( collections = storedCollections, prices = storedPrices, ) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ saveCollectionsInRuntime( userWalletId = userWalletId, network = network, collections = it, ) saveCollectionsInPersistence( userWalletId = userWalletId, network = network, collections = it, ) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ saveFailedStateInRuntime( userWalletId = userWalletId, network = network, error = it, ) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ storedCollections.copy( content = it, ) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ val (assetId, _) = it assetIdConverter.convert(assetId) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ val (assetId, price) = it val nftCurrency = getNFTCurrency(network) NFTSalePrice.Value( assetId = assetId, value = price.value, fiatValue = null, symbol = nftCurrency.symbol, decimals = nftCurrency.decimals, ) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ val assetId = assetIdConverter.convert(it.identifier) val price = getNFTRuntimeStore(userWalletId, network).getSalePriceSync(assetId) if (price is NFTSalePrice.Empty || price is NFTSalePrice.Error) { refreshSalePrice(userWalletId, network, sdkCollectionId, it.identifier) } } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ val sdkAssetId = assetIdConverter.convertBack(assetId) saveSalePriceInPersistence(userWalletId, network, sdkAssetId, it) } - NamedArguments:DefaultNFTRepository.kt$DefaultNFTRepository$refreshSalePrice(userWalletId, network, sdkCollectionId, it.identifier) - NamedArguments:DefaultNFTRepository.kt$DefaultNFTRepository$saveSalePriceInPersistence(userWalletId, network, sdkAssetId, it) - NoNameShadowing:DefaultNFTRepository.kt$DefaultNFTRepository${ it.id !is NFTCollection.Identifier.Unknown } - SuspendFunSwallowedCancellation:DefaultNFTRepository.kt$DefaultNFTRepository$runCatching - SuspendFunWithFlowReturnType:DefaultNFTRepository.kt$DefaultNFTRepository$suspend - UnnecessaryLet:DefaultNFTRepository.kt$DefaultNFTRepository$let { prices -> prices .mapKeys { val (assetId, _) = it assetIdConverter.convert(assetId) } .mapValues { val (assetId, price) = it val nftCurrency = getNFTCurrency(network) NFTSalePrice.Value( assetId = assetId, value = price.value, fiatValue = null, symbol = nftCurrency.symbol, decimals = nftCurrency.decimals, ) } } - - 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 851849db2a..b16adf3821 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 @@ -16,8 +16,8 @@ import com.tangem.datasource.local.nft.converter.NFTSdkAssetIdentifierConverter import com.tangem.datasource.local.nft.converter.NFTSdkAssetSalePriceConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.extensions.canHandleToken +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -47,13 +47,13 @@ import javax.inject.Inject import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection -@Suppress("LargeClass", "LongParameterList") +@Suppress("LargeClass", "LongParameterList", "SuspendFunWithFlowReturnType") internal class DefaultNFTRepository @Inject constructor( private val nftPersistenceStoreFactory: NFTPersistenceStoreFactory, private val nftRuntimeStoreFactory: NFTRuntimeStoreFactory, private val walletManagersFacade: WalletManagersFacade, private val dispatchers: CoroutineDispatcherProvider, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val networkFactory: NetworkFactory, private val excludedBlockchains: ExcludedBlockchains, @ApplicationContext private val context: Context, @@ -87,7 +87,7 @@ internal class DefaultNFTRepository @Inject constructor( ): NFTSalePrice = withContext(dispatchers.io) { val salePriceConverter = NFTSdkAssetSalePriceConverter(assetId) - runCatching { + runSuspendCatching { saveSalePriceInRuntime(userWalletId, network, NFTSalePrice.Loading(assetId)) val sdkPrice = walletManagersFacade.getNFTSalePrice( @@ -108,9 +108,14 @@ internal class DefaultNFTRepository @Inject constructor( saveSalePriceInRuntime(userWalletId, network, salePrice) - sdkPrice?.let { + sdkPrice?.let { price -> val sdkAssetId = assetIdConverter.convertBack(assetId) - saveSalePriceInPersistence(userWalletId, network, sdkAssetId, it) + saveSalePriceInPersistence( + userWalletId = userWalletId, + network = network, + assetId = sdkAssetId, + salePrice = price, + ) } salePrice @@ -162,41 +167,46 @@ internal class DefaultNFTRepository @Inject constructor( expireAssets(userWalletId, network, collectionId) - assets.forEach { - val assetId = assetIdConverter.convert(it.identifier) + assets.forEach { asset -> + val assetId = assetIdConverter.convert(asset.identifier) val price = getNFTRuntimeStore(userWalletId, network).getSalePriceSync(assetId) if (price is NFTSalePrice.Empty || price is NFTSalePrice.Error) { - refreshSalePrice(userWalletId, network, sdkCollectionId, it.identifier) + refreshSalePrice( + userWalletId = userWalletId, + network = network, + sdkCollectionId = sdkCollectionId, + sdkAssetId = asset.identifier, + ) } } getNFTPersistenceStore(userWalletId, network) .getCollectionsSync() - ?.map { - if (it.identifier == sdkCollectionId) { - it.copy(assets = assets) + ?.map { collection -> + if (collection.identifier == sdkCollectionId) { + collection.copy(assets = assets) } else { - it + collection } } - ?.let { + ?.let { collections -> saveCollectionsInRuntime( userWalletId = userWalletId, network = network, - collections = it, + collections = collections, ) saveCollectionsInPersistence( userWalletId = userWalletId, network = network, - collections = it, + collections = collections, ) } - }.onLeft { - if (it !is UnsupportedOperationException) { + }.onLeft { throwable -> + if (throwable !is UnsupportedOperationException) { saveFailedStateInRuntime( userWalletId = userWalletId, network = network, - error = it, + error = throwable, ) } } @@ -207,13 +217,13 @@ internal class DefaultNFTRepository @Inject constructor( network.canHandleNFTs(userWalletId) override suspend fun getNFTSupportedNetworks(userWalletId: UserWalletId): List { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) return Blockchain .entries .filter { !it.isTestnet() } - .mapNotNull { + .mapNotNull { blockchain -> networkFactory.create( - blockchain = it, + blockchain = blockchain, extraDerivationPath = null, userWallet = userWallet, ) @@ -280,11 +290,11 @@ internal class DefaultNFTRepository @Inject constructor( ) } } - }.onLeft { + }.onLeft { throwable -> saveFailedStateInRuntime( userWalletId = userWalletId, network = network, - error = it, + error = throwable, ) } }.saveIn(getNetworkJobHolder(network)) @@ -334,11 +344,7 @@ internal class DefaultNFTRepository @Inject constructor( val storedCollections = runtimeStore.getCollectionsSync() val expiredCollections = storedCollections .changeCollectionAssetsStatusSource(collectionId, StatusSource.CACHE) - .let { - storedCollections.copy( - content = it, - ) - } + .let { storedCollections.copy(content = it) } runtimeStore.saveCollections(expiredCollections) } @@ -427,6 +433,7 @@ internal class DefaultNFTRepository @Inject constructor( } } + // TODO: remove suspend private suspend fun getNFTRuntimeStore(userWalletId: UserWalletId, network: Network): NFTRuntimeStore { val storeId = (userWalletId to network).formatted() return nftRuntimeStoresMutex.withLock { @@ -446,44 +453,41 @@ internal class DefaultNFTRepository @Inject constructor( private suspend fun getStoredCollections(userWalletId: UserWalletId, network: Network) = getNFTPersistenceStore(userWalletId, network) .getCollectionsSync() - .let { + .let { collections -> NFTCollections( network = network, content = NFTCollections.Content.Collections( - collections = it + collections = collections ?.map { collection -> nftSdkCollectionConverter.convert(network to collection) } - ?.filter { - it.id !is NFTCollection.Identifier.Unknown - }, + ?.filter { it.id !is NFTCollection.Identifier.Unknown }, source = StatusSource.CACHE, ), ) } - private suspend fun getStoredPrices(userWalletId: UserWalletId, network: Network) = - getNFTPersistenceStore(userWalletId, network) + private suspend fun getStoredPrices( + userWalletId: UserWalletId, + network: Network, + ): Map { + val prices = getNFTPersistenceStore(userWalletId, network) .getSalePricesSync() .orEmpty() - .let { prices -> - prices - .mapKeys { - val (assetId, _) = it - assetIdConverter.convert(assetId) - } - .mapValues { - val (assetId, price) = it - val nftCurrency = getNFTCurrency(network) - NFTSalePrice.Value( - assetId = assetId, - value = price.value, - fiatValue = null, - symbol = nftCurrency.symbol, - decimals = nftCurrency.decimals, - ) - } + + return prices + .mapKeys { (assetId, _) -> assetIdConverter.convert(assetId) } + .mapValues { (assetId, price) -> + val nftCurrency = getNFTCurrency(network) + NFTSalePrice.Value( + assetId = assetId, + value = price.value, + fiatValue = null, + symbol = nftCurrency.symbol, + decimals = nftCurrency.decimals, + ) } + } private fun NFTCollections.changeStatusSource(source: StatusSource) = copy( content = when (val content = content) { @@ -503,11 +507,11 @@ internal class DefaultNFTRepository @Inject constructor( .copy( collections = content .collections - ?.map { - if (it.id == collectionId) { - it.changeAssetsStatusSource(source) + ?.map { collection -> + if (collection.id == collectionId) { + collection.changeAssetsStatusSource(source) } else { - it + collection } }, ) @@ -562,7 +566,7 @@ internal class DefaultNFTRepository @Inject constructor( } private fun Network.canHandleNFTs(userWalletId: UserWalletId): Boolean { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) val blockchain = Blockchain.fromNetworkId(backendId) ?: return false return blockchain.canHandleNFTs() && diff --git a/data/nft/src/test/kotlin/com/tangem/data/nft/NFTCleanerTest.kt b/data/nft/src/test/kotlin/com/tangem/data/nft/NFTCleanerTest.kt index 1a27526992..5056f8420f 100644 --- a/data/nft/src/test/kotlin/com/tangem/data/nft/NFTCleanerTest.kt +++ b/data/nft/src/test/kotlin/com/tangem/data/nft/NFTCleanerTest.kt @@ -24,7 +24,7 @@ class NFTCleanerTest { nftRuntimeStoreFactory = nftRuntimeStoreFactory, walletManagersFacade = mockk(), dispatchers = mockk(), - userWalletsStore = mockk(), + userWalletsListRepository = mockk(), networkFactory = mockk(), excludedBlockchains = mockk(), context = mockk(), 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 4a1a5c9a1b..a8c3926c66 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 @@ -17,7 +17,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrap import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.datasource.local.token.P2PEthPoolVaultsStore import com.tangem.datasource.local.token.StakingYieldsStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWallet @@ -41,20 +41,20 @@ import javax.inject.Inject * * Supports both StakeKit and P2PEthPool staking providers. * - * @property userWalletsStore user wallets store - * @property stakingYieldsStore staking yields store - * @property stakeKitBalancesStore staking balances store (StakeKit) - * @property p2PEthPoolBalancesStore P2PEthPool balances store - * @property stakeKitApi stake kit API - * @property p2pEthPoolApi P2PEthPool API - * @property p2pEthPoolVaultsStore P2PEthPool vaults store - * @property dispatchers dispatchers + * @property userWalletsListRepository repository of user wallets + * @property stakingYieldsStore staking yields store + * @property stakeKitBalancesStore staking balances store (StakeKit) + * @property p2PEthPoolBalancesStore P2PEthPool balances store + * @property stakeKitApi stake kit API + * @property p2pEthPoolApi P2PEthPool API + * @property p2pEthPoolVaultsStore P2PEthPool vaults store + * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ @Suppress("LongParameterList") internal class DefaultMultiStakingBalanceFetcher @Inject constructor( - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val stakingYieldsStore: StakingYieldsStore, private val stakeKitBalancesStore: StakeKitBalancesStore, private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore, @@ -238,7 +238,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( } private inline fun checkIsSupportedByWalletOrElse(userWalletId: UserWalletId, ifNotSupported: (Throwable) -> Unit) { - val maybeUserWallet = userWalletsStore.getSyncOrNull(key = userWalletId).toOption() + val maybeUserWallet = userWalletsListRepository.getSyncOrNull(id = userWalletId).toOption() val isSupportedByWallet = maybeUserWallet.isSome(UserWallet::isMultiCurrency) diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt index 65412ad690..d37c4d2c87 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt @@ -14,7 +14,7 @@ import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.token.P2PEthPoolVaultsStore import com.tangem.datasource.local.token.StakingYieldsStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher @@ -33,7 +33,7 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DefaultMultiStakingBalanceFetcherTest { - private val userWalletsStore: UserWalletsStore = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk() private val stakingYieldsStore: StakingYieldsStore = mockk() private val stakeKitBalancesStore: StakeKitBalancesStore = mockk(relaxUnitFun = true) private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore = mockk(relaxUnitFun = true) @@ -42,7 +42,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore = mockk() private val fetcher = DefaultMultiStakingBalanceFetcher( - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, stakingYieldsStore = stakingYieldsStore, stakeKitBalancesStore = stakeKitBalancesStore, p2PEthPoolBalancesStore = p2PEthPoolBalancesStore, @@ -54,7 +54,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { @BeforeEach fun resetMocks() { - clearMocks(userWalletsStore, stakingYieldsStore, stakeKitBalancesStore, stakeKitApi) + clearMocks(userWalletsListRepository, stakingYieldsStore, stakeKitBalancesStore, stakeKitApi) } @Test @@ -62,7 +62,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId)) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields @@ -80,7 +80,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.getSyncOrNull(params.userWalletId) stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getMultipleYieldBalances(requests) @@ -97,7 +97,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet val yields = listOf(MockYieldDTOFactory.create(tonId)) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields @@ -112,7 +112,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.getSyncOrNull(params.userWalletId) stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId)) @@ -129,13 +129,13 @@ internal class DefaultMultiStakingBalanceFetcherTest { val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet // Actual val actual = fetcher.invoke(params) // Assert - coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) } + coVerifyOrder { userWalletsListRepository.getSyncOrNull(params.userWalletId) } coVerify(inverse = true) { stakeKitBalancesStore.refresh(userWalletId = any(), stakingIds = any()) @@ -155,13 +155,13 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns null + coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns null // Actual val actual = fetcher.invoke(params) // Assert - coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) } + coVerifyOrder { userWalletsListRepository.getSyncOrNull(params.userWalletId) } coVerify(inverse = true) { stakeKitBalancesStore.refresh(userWalletId = any(), stakingIds = any()) @@ -181,7 +181,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingYieldsStore.getSyncWithTimeout() } returns null // Actual @@ -189,7 +189,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.getSyncOrNull(params.userWalletId) stakeKitBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -210,7 +210,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingYieldsStore.getSyncWithTimeout() } returns emptyList() // Actual @@ -218,7 +218,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.getSyncOrNull(params.userWalletId) stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -239,7 +239,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet val yields = listOf( MockYieldDTOFactory.create(tonId).copy(id = null), @@ -252,7 +252,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.getSyncOrNull(params.userWalletId) stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -273,7 +273,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet val yields = listOf(MockYieldDTOFactory.create(StakingID(integrationId = "polygon", address = "0x1"))) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields @@ -283,7 +283,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.getSyncOrNull(params.userWalletId) stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -310,7 +310,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId)) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields @@ -328,7 +328,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.getSyncOrNull(params.userWalletId) stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getMultipleYieldBalances(requests) diff --git a/data/txhistory/build.gradle.kts b/data/txhistory/build.gradle.kts index d7bceabb15..78b3545254 100644 --- a/data/txhistory/build.gradle.kts +++ b/data/txhistory/build.gradle.kts @@ -15,15 +15,18 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.datasource) implementation(projects.core.pagination) + implementation(projects.domain.legacy) + implementation(projects.domain.common) implementation(projects.domain.walletManager) - implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory) implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) + implementation(projects.libs.blockchainSdk) + implementation(deps.kotlin.coroutines) implementation(deps.androidx.paging.runtime) implementation(deps.timber) diff --git a/data/txhistory/detekt-baseline-debug.xml b/data/txhistory/detekt-baseline-debug.xml index 3dfda1c580..02aca3d1f9 100644 --- a/data/txhistory/detekt-baseline-debug.xml +++ b/data/txhistory/detekt-baseline-debug.xml @@ -3,11 +3,7 @@ BooleanPropertyNaming:TxHistoryPagingSource.kt$TxHistoryPagingSource.Params$val refresh: Boolean - MaxChainedCallsOnSameLine:DefaultTxHistoryRepository.kt$DefaultTxHistoryRepository$walletManager?.wallet?.recentTransactions?.last()?.hash.orEmpty() MultilineLambdaItParameter:TxHistoryPageBatchFetcher.kt$TxHistoryPageBatchFetcher${ currentCoroutineContext().ensureActive() BatchFetchResult.Error(it) } - NamedArguments:DefaultTxHistoryRepository.kt$DefaultTxHistoryRepository$Params(userWalletId, currency, pageSize, refresh) - NamedArguments:TxHistoryDataModule.kt$TxHistoryDataModule$DefaultTxHistoryRepository( cacheRegistry, walletManagersFacade, userWalletsStore, txHistoryItemsStore, dispatchers, ) SuspendFunSwallowedCancellation:TxHistoryPageBatchFetcher.kt$TxHistoryPageBatchFetcher$runCatching - UseOrEmpty:DefaultTxHistoryRepository.kt$DefaultTxHistoryRepository$txs ?: emptyList() diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt index 1a756f54e1..7eab581f07 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt @@ -4,7 +4,7 @@ import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.txhistory.repository.DefaultTxHistoryRepository import com.tangem.data.txhistory.repository.RefactoredTxHistoryRepository import com.tangem.datasource.local.txhistory.TxHistoryItemsStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.txhistory.repository.TxHistoryRepository import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2 import com.tangem.domain.walletmanager.WalletManagersFacade @@ -24,15 +24,15 @@ internal object TxHistoryDataModule { fun provideTxHistoryRepository( cacheRegistry: CacheRegistry, walletManagersFacade: WalletManagersFacade, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, txHistoryItemsStore: TxHistoryItemsStore, dispatchers: CoroutineDispatcherProvider, ): TxHistoryRepository = DefaultTxHistoryRepository( - cacheRegistry, - walletManagersFacade, - userWalletsStore, - txHistoryItemsStore, - dispatchers, + cacheRegistry = cacheRegistry, + walletManagersFacade = walletManagersFacade, + userWalletsListRepository = userWalletsListRepository, + txHistoryItemsStore = txHistoryItemsStore, + dispatchers = dispatchers, ) @Provides 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 b668562534..8c026a475d 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 @@ -8,10 +8,9 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource import com.tangem.datasource.local.txhistory.TxHistoryItemsStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.TxInfo -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.txhistory.models.Page import com.tangem.domain.txhistory.models.TxHistoryState @@ -27,7 +26,7 @@ import timber.log.Timber class DefaultTxHistoryRepository( private val cacheRegistry: CacheRegistry, private val walletManagersFacade: WalletManagersFacade, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val txHistoryItemsStore: TxHistoryItemsStore, private val dispatchers: CoroutineDispatcherProvider, ) : TxHistoryRepository { @@ -35,7 +34,7 @@ class DefaultTxHistoryRepository( override suspend fun getTxHistoryItemsCount(userWalletId: UserWalletId, currency: CryptoCurrency): Int { return withContext(dispatchers.io) { - val userWallet = getUserWallet(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) val state = walletManagersFacade.getTxHistoryState( userWalletId = userWallet.walletId, currency = currency, @@ -63,7 +62,12 @@ class DefaultTxHistoryRepository( ), pagingSourceFactory = { TxHistoryPagingSource( - sourceParams = TxHistoryPagingSource.Params(userWalletId, currency, pageSize, refresh), + sourceParams = TxHistoryPagingSource.Params( + userWalletId = userWalletId, + currency = currency, + pageSize = pageSize, + refresh = refresh, + ), txHistoryItemsStore = txHistoryItemsStore, walletManagersFacade = walletManagersFacade, cacheRegistry = cacheRegistry, @@ -98,11 +102,12 @@ class DefaultTxHistoryRepository( skipCache = shouldRefresh, block = { fetchFixedSizeTxHistoryItems(userWalletId, currency, pageSize) }, ) - val txs = txHistoryItemsStore.getSyncOrNull( + + txHistoryItemsStore.getSyncOrNull( key = TxHistoryItemsStore.Key(userWalletId, currency), page = Page.Initial, - )?.items - txs ?: emptyList() + ) + ?.items.orEmpty() } catch (e: Throwable) { Timber.e(e, "Unable to load the transaction history for the requested page: ${Page.Initial}") emptyList() @@ -127,10 +132,4 @@ class DefaultTxHistoryRepository( txHistoryItemsStore.store(TxHistoryItemsStore.Key(userWalletId, currency), wrappedItems) } - - private fun getUserWallet(userWalletId: UserWalletId): UserWallet { - return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "Unable to find user wallet with provided ID: $userWalletId" - } - } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWalletConnectRepository.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWalletConnectRepository.kt index 62d9f25d28..0aa06feafc 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWalletConnectRepository.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWalletConnectRepository.kt @@ -1,22 +1,15 @@ package com.tangem.data.walletconnect -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.walletconnect.repository.WalletConnectRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext internal class DefaultWalletConnectRepository( - private val userWalletsStore: UserWalletsStore, - private val dispatchers: CoroutineDispatcherProvider, + private val userWalletsListRepository: UserWalletsListRepository, ) : WalletConnectRepository { - override suspend fun checkIsAvailable(userWalletId: UserWalletId): Boolean = withContext(dispatchers.io) { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "User wallet with id $userWalletId not found" - } - - userWallet.isMultiCurrency + override suspend fun checkIsAvailable(userWalletId: UserWalletId): Boolean { + return userWalletsListRepository.getSyncStrict(userWalletId).isMultiCurrency } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index 9b04683948..65749b77cb 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -19,12 +19,12 @@ import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.data.walletconnect.utils.WcNetworksConverter import com.tangem.data.walletconnect.utils.WcScope import com.tangem.datasource.di.SdkMoshi -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.account.supplier.SingleAccountSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.WcRequestService @@ -51,10 +51,9 @@ internal object WalletConnectDataModule { @Provides @Singleton fun providesWalletConnectRepository( - userWalletsStore: UserWalletsStore, - dispatchers: CoroutineDispatcherProvider, + userWalletsListRepository: UserWalletsListRepository, ): WalletConnectRepository { - return DefaultWalletConnectRepository(userWalletsStore, dispatchers) + return DefaultWalletConnectRepository(userWalletsListRepository) } @Provides 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 b2b04e77fa..cd3223e10a 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 @@ -28,8 +28,8 @@ import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.walletmanager.utils.* import com.tangem.datasource.asset.loader.AssetLoader -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -60,7 +60,7 @@ import javax.inject.Inject @Suppress("LargeClass", "TooManyFunctions") internal class DefaultWalletManagersFacade @Inject constructor( private val walletManagersStore: WalletManagersStore, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val assetLoader: AssetLoader, private val dispatchers: CoroutineDispatcherProvider, private val gaslessTransactionRepository: GaslessTransactionRepository, @@ -300,7 +300,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( } } - private fun getUserWallet(userWalletId: UserWalletId) = userWalletsStore.getSyncStrict(userWalletId) + private fun getUserWallet(userWalletId: UserWalletId) = userWalletsListRepository.getSyncStrict(userWalletId) private suspend fun getAndUpdateWalletManager( userWallet: UserWallet, diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt index 9bc5ac4c5d..125322a147 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -8,7 +8,7 @@ import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.StartReferralBody -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId @@ -27,8 +27,8 @@ typealias ExternalReferralRepository = com.tangem.domain.referral.ReferralReposi internal class ReferralRepositoryImpl @Inject constructor( private val referralApi: TangemTechApi, private val referralConverter: ReferralConverter, - private val coroutineDispatcher: CoroutineDispatcherProvider, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, + private val dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, ) : ReferralRepository, ExternalReferralRepository { @@ -38,7 +38,7 @@ internal class ReferralRepositoryImpl @Inject constructor( private val referralStatus: ConcurrentHashMap = ConcurrentHashMap() override suspend fun getReferralData(walletId: String): ReferralData { - return withContext(coroutineDispatcher.io) { + return withContext(dispatchers.io) { val referralData = referralConverter.convert( referralApi.getReferralStatus( walletId = walletId, @@ -76,7 +76,7 @@ internal class ReferralRepositoryImpl @Inject constructor( tokenId: String, address: String, ): ReferralData { - return withContext(coroutineDispatcher.io) { + return withContext(dispatchers.io) { val referralData = referralConverter.convert( referralApi.startReferral( startReferralBody = StartReferralBody( @@ -97,9 +97,7 @@ internal class ReferralRepositoryImpl @Inject constructor( tokenData: TokenData, accountIndex: DerivationIndex?, ): CryptoCurrency? { - val userWallet = withContext(coroutineDispatcher.io) { - userWalletsStore.getSyncOrNull(userWalletId) ?: error("Wallet $userWalletId not found") - } + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) val blockchain = Blockchain.fromNetworkId(tokenData.networkId) ?: error("Blockchain ${tokenData.networkId} not found") diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt index 2170e1651d..af0876c251 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt @@ -3,7 +3,7 @@ package com.tangem.feature.referral.di import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.feature.referral.converters.ReferralConverter import com.tangem.feature.referral.data.DefaultMobileWalletPromoRepository import com.tangem.feature.referral.data.ExternalReferralRepository @@ -26,15 +26,15 @@ class ReferralRepositoryModule { fun provideReferralRepository( tangemTechApi: TangemTechApi, referralConverter: ReferralConverter, - coroutineDispatcherProvider: CoroutineDispatcherProvider, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, + dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, ): ReferralRepository { return ReferralRepositoryImpl( referralApi = tangemTechApi, referralConverter = referralConverter, - coroutineDispatcher = coroutineDispatcherProvider, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, + dispatchers = dispatchers, excludedBlockchains = excludedBlockchains, ) } @@ -44,15 +44,15 @@ class ReferralRepositoryModule { fun provideExternalReferralRepository( tangemTechApi: TangemTechApi, referralConverter: ReferralConverter, - coroutineDispatcherProvider: CoroutineDispatcherProvider, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, + dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, ): ExternalReferralRepository { return ReferralRepositoryImpl( referralApi = tangemTechApi, referralConverter = referralConverter, - coroutineDispatcher = coroutineDispatcherProvider, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, + dispatchers = dispatchers, excludedBlockchains = excludedBlockchains, ) } 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 2b464a5d59..e6be947657 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 @@ -25,7 +25,7 @@ import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.exchangeservice.swap.ExpressUtils import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.models.currency.CryptoCurrency @@ -52,7 +52,7 @@ internal class DefaultSwapRepository( private val tangemExpressApi: TangemExpressApi, private val coroutineDispatcher: CoroutineDispatcherProvider, private val walletManagersFacade: WalletManagersFacade, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val errorsDataConverter: ErrorsDataConverter, private val dataSignatureVerifier: DataSignatureVerifier, private val appPreferencesStore: AppPreferencesStore, @@ -406,14 +406,14 @@ internal class DefaultSwapRepository( ) } - override fun getNativeTokenForNetwork(networkId: String): CryptoCurrency { + override suspend fun getNativeTokenForNetwork(networkId: String): CryptoCurrency { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } return requireNotNull( cryptoCurrencyFactory.createCoin( blockchain = blockchain, extraDerivationPath = null, - userWallet = requireNotNull(userWalletsStore.selectedUserWalletOrNull), + userWallet = requireNotNull(userWalletsListRepository.selectedUserWalletSync()), ), ) } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index bb9b016c41..91ff6e2f27 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -9,9 +9,9 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.swap.DefaultSwapRepository @@ -37,7 +37,7 @@ internal class SwapDataModule { coroutineDispatcher: CoroutineDispatcherProvider, dataSignature: DataSignatureVerifier, walletManagerFacade: WalletManagersFacade, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, errorsDataConverter: ErrorsDataConverter, @NetworkMoshi moshi: Moshi, excludedBlockchains: ExcludedBlockchains, @@ -48,7 +48,7 @@ internal class SwapDataModule { tangemExpressApi = tangemExpressApi, coroutineDispatcher = coroutineDispatcher, walletManagersFacade = walletManagerFacade, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, errorsDataConverter = errorsDataConverter, dataSignatureVerifier = dataSignature, moshi = moshi, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index d8ba2cc67b..1c31e6c77a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -60,12 +60,16 @@ interface SwapInteractor { /** * Starts swap transaction, perform sign transaction * - * @param networkId network for tokens - * @param swapStateData tx data to swap, contains data to sign - * @param currencyToSend [Currency] - * @param currencyToGet [Currency] + * @param swapProvider swap provider to use + * @param swapData tx data to swap, contains data to sign + * @param currencyToSend crypto currency to send + * @param currencyToGet crypto currency to get + * @param fromAccount account from which swap will be made + * @param toAccount account to which receive token * @param amountToSwap amount to swap + * @param includeFeeInAmount flag to include fee in amount * @param fee for tx (can be null only for tangem pay withdrawal) + * @param expressOperationType type of express operation * @return [SwapTransactionState] */ @@ -119,7 +123,7 @@ interface SwapInteractor { isReverseFromTo: Boolean, ): AccountSwapCurrency? - fun getNativeToken(networkId: String): CryptoCurrency + suspend fun getNativeToken(networkId: String): CryptoCurrency @Suppress("LongParameterList") suspend fun storeSwapTransaction( 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 8b1c5ff4a5..046dc76c7f 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 @@ -1338,7 +1338,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( } } - override fun getNativeToken(networkId: String): CryptoCurrency { + override suspend fun getNativeToken(networkId: String): CryptoCurrency { return repository.getNativeTokenForNetwork(networkId) } @@ -2553,7 +2553,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val areAllQuotesFound = cachedQuotes?.all { quote -> quote.value !is QuoteStatus.Empty } == true - if (areAllQuotesFound) return@runSuspendCatching cachedQuotes.orEmpty() + if (areAllQuotesFound) return@runSuspendCatching cachedQuotes val currenciesIds = if (cachedQuotes.isNullOrEmpty()) { this@getQuotesOrEmpty @@ -2612,11 +2612,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( sealed class TxFeeSealedState { class Legacy(val txFeeState: TxFeeState, val selectedFee: FeeType) : TxFeeSealedState() class Component(val txFee: TxFee.FeeComponent) : TxFeeSealedState() - - fun getTxFeeStateOrNull() = when (this) { - is Component -> null - is Legacy -> txFeeState - } } sealed class TransactionFeeResult { 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 fedf814baa..bb40242f87 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 @@ -6,12 +6,7 @@ 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.ExchangeStatusModel -import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo -import com.tangem.feature.swap.domain.models.domain.PairsWithProviders -import com.tangem.feature.swap.domain.models.domain.QuoteModel -import com.tangem.feature.swap.domain.models.domain.RateType -import com.tangem.feature.swap.domain.models.domain.SwapDataModel +import com.tangem.feature.swap.domain.models.domain.* import java.math.BigDecimal interface SwapRepository { @@ -87,5 +82,5 @@ interface SwapRepository { payInExtraId: String?, ): Either - fun getNativeTokenForNetwork(networkId: String): CryptoCurrency + suspend fun getNativeTokenForNetwork(networkId: String): CryptoCurrency } \ No newline at end of file From d5f9410905449a3e8c5e9b6f0ceed20568d541fb Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Feb 2026 11:23:37 +0100 Subject: [PATCH 031/106] Updated on 2026-08-14 --- .../api/tangemTech/TangemTechApi.kt | 4 +- .../api/tangemTech/models/EarnResponse.kt | 2 +- .../earn/repository/DefaultEarnRepository.kt | 12 +- .../batch/EarnTokensBatchFetcher.kt | 2 +- .../domain/earn/model/EarnTokensListConfig.kt | 6 +- .../domain/earn/repository/EarnRepository.kt | 2 +- .../earn/usecase/FetchEarnNetworksUseCase.kt | 4 +- .../tangem/domain/models/earn/EarnNetwork.kt | 4 + .../tangem/domain/models/earn/EarnToken.kt | 2 +- .../feed/components/FeedEntryChildFactory.kt | 1 + .../components/earn/DefaultEarnComponent.kt | 33 ++ ...nTokenWithCurrencyToListItemUMConverter.kt | 50 +++ .../features/feed/model/earn/EarnModel.kt | 421 +++++++++++------- .../model/earn/EarnTokensListConfigFactory.kt | 37 ++ .../model/earn/state/EarnStateController.kt | 48 ++ .../state/converter/EarnNetworksConverter.kt | 22 + .../ClearEarnUMFilterStateTransformer.kt | 28 ++ ...rnNetworkFilterSelectedStateTransformer.kt | 18 + .../EarnNetworkFilterStateTransformer.kt | 21 + .../EarnTypeFilterSelectedStateTransformer.kt | 18 + .../EarnTypeFilterStateTransformer.kt | 21 + .../state/transformers/EarnUMTransformer.kt | 7 + ...UpdateBestOpportunitiesStateTransformer.kt | 13 + .../UpdateEarnUMInitialStateTransformer.kt | 23 + .../UpdateMostlyUsedStateTransformer.kt | 38 ++ .../statemanager/EarnListBatchFlowManager.kt | 105 +++++ .../earn/statemanager/EarnListStateManager.kt | 28 ++ .../UpdateEarnStateTransformer.kt | 73 +-- .../statemanager/NewsListBatchFlowManager.kt | 22 +- .../features/feed/ui/earn/EarnContent.kt | 113 +++-- .../EarnFilterByNetworkBottomSheet.kt | 16 +- .../feed/ui/earn/state/EarnFilterNetworkUM.kt | 2 +- .../features/feed/ui/earn/state/EarnListUM.kt | 5 +- .../feed/ui/feed/components/EarnBlock.kt | 11 +- .../component/DefaultTokenReceiveComponent.kt | 4 +- 35 files changed, 892 insertions(+), 324 deletions(-) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/EarnTokenWithCurrencyToListItemUMConverter.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/converter/EarnNetworksConverter.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/ClearEarnUMFilterStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterSelectedStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnUMTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateBestOpportunitiesStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListStateManager.kt 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 0b0731ea4d..cf0d66f054 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 @@ -213,10 +213,10 @@ interface TangemTechApi { @Query("page") page: String? = null, @Query("limit") limit: Int? = null, @Query("type") type: String? = null, - @Query("network") network: String? = null, + @Query("networkIds") networks: List? = null, ): ApiResponse @GET("v1/earn/networks") - suspend fun getEarnNetworks(@Query("type") type: String): ApiResponse + suspend fun getEarnNetworks(@Query("type") type: String? = null): ApiResponse // endregion } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt index 47ec19a2ff..b123c7d912 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt @@ -23,7 +23,7 @@ data class EarnTokenResponse( @Json(name = "id") val id: String, @Json(name = "symbol") val symbol: String, @Json(name = "name") val name: String, - @Json(name = "address") val address: String, + @Json(name = "address") val address: String? = null, @Json(name = "decimalCount") val decimalCount: Int? = null, ) diff --git a/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt b/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt index 7a78b887bb..53c36696d4 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt @@ -62,13 +62,16 @@ internal class DefaultEarnRepository( ).toBatchFlow() } - override suspend fun fetchEarnNetworks(type: String) { + override suspend fun fetchEarnNetworks() { runCatching(dispatchers.io) { - val response = tangemTechApi.getEarnNetworks(type = type).getOrThrow() + val response = tangemTechApi.getEarnNetworks().getOrThrow() val items = response.items.map { dto -> + val blockchain = Blockchain.fromNetworkId(dto.networkId) EarnNetwork( networkId = dto.networkId, isAdded = false, + fullName = blockchain?.fullName.orEmpty(), + symbol = blockchain?.currency.orEmpty(), ) } earnNetworksStore.store(Either.Right(items)) @@ -136,7 +139,8 @@ internal fun createCryptoCurrencyForEarnToken( cryptoCurrencyFactory: CryptoCurrencyFactory, ): CryptoCurrency? { val blockchain = Blockchain.fromNetworkId(earnToken.networkId) ?: Blockchain.Unknown - return if (earnToken.token.address.isEmpty()) { + val address = earnToken.token.address + return if (address == null) { cryptoCurrencyFactory.createCoin( blockchain = blockchain, extraDerivationPath = null, @@ -157,7 +161,7 @@ internal fun createCryptoCurrencyForEarnToken( name = earnToken.token.name, symbol = earnToken.token.symbol, decimals = earnToken.token.decimalCount ?: blockchain.decimals(), - contractAddress = earnToken.token.address, + contractAddress = address, ) } } \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt b/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt index 85d104f727..aeead13270 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt @@ -100,7 +100,7 @@ internal class EarnTokensBatchFetcher( page = page.toString(), limit = limit, type = params.type, - network = params.network, + networks = params.networks, ).getOrThrow() val userWallet = userWalletsListRepository.selectedUserWallet.value diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTokensListConfig.kt b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTokensListConfig.kt index ab150727ee..20320ace05 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTokensListConfig.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTokensListConfig.kt @@ -6,12 +6,12 @@ import kotlinx.serialization.Serializable * Config of request earn tokens. * * @param type of Earn (ex: yield, staking). - * @param network networkId. + * @param networks list of networkId. * @param isForEarn flag to load mostly used tokens. */ @Serializable data class EarnTokensListConfig( - val type: String, - val network: String, + val type: String?, + val networks: List?, val isForEarn: Boolean, ) \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt b/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt index 5727b0cd34..e20bab51ec 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt @@ -13,7 +13,7 @@ interface EarnRepository { /** * Load all networks for Earn and store it in data store. */ - suspend fun fetchEarnNetworks(type: String) + suspend fun fetchEarnNetworks() fun observeEarnNetworks(): Flow diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchEarnNetworksUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchEarnNetworksUseCase.kt index fda4a392d4..aa426437e7 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchEarnNetworksUseCase.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchEarnNetworksUseCase.kt @@ -7,9 +7,9 @@ class FetchEarnNetworksUseCase( private val repository: EarnRepository, ) { - suspend operator fun invoke(type: String): Either { + suspend operator fun invoke(): Either { return Either.catch { - repository.fetchEarnNetworks(type = type) + repository.fetchEarnNetworks() } } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetwork.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetwork.kt index 04d3edbc79..114bbad106 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetwork.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetwork.kt @@ -5,10 +5,14 @@ import kotlinx.serialization.Serializable /** * Model for networks in Earn. * @param networkId - id of network + * @param fullName - full name of network + * @param symbol - symbol of network * @param isAdded - is exists in any user's wallet */ @Serializable data class EarnNetwork( val networkId: String, + val fullName: String, + val symbol: String, val isAdded: Boolean, ) \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt index 523ba3ac45..e3fc112731 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt @@ -22,7 +22,7 @@ data class EarnToken( val tokenId: String, val tokenSymbol: String, val tokenName: String, - val tokenAddress: String, + val tokenAddress: String?, val decimalCount: Int?, ) 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 9deae9964b..02b1c82ab7 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 @@ -109,6 +109,7 @@ internal class FeedEntryChildFactory @Inject constructor( DefaultEarnComponent( appComponentContext = appComponentContext, params = child.params, + addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent, ) } } 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 ad41d329a1..3da82e53fe 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 @@ -5,15 +5,22 @@ import androidx.compose.runtime.State 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.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.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.features.feed.components.feed.FeedPortfolioRoute +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.earn.EarnContent import kotlinx.serialization.Serializable @@ -21,10 +28,18 @@ import kotlinx.serialization.Serializable internal class DefaultEarnComponent( appComponentContext: AppComponentContext, private val params: Params, + private val addToPortfolioComponentFactory: AddToPortfolioPreselectedDataComponent.Factory, ) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext { private val earnModel = getOrCreateModel(params = params) + private val bottomSheetSlot = childSlot( + source = earnModel.bottomSheetNavigation, + serializer = FeedPortfolioRoute.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + @Composable override fun Title(bottomSheetState: State) { val background = LocalMainBottomSheetColor.current.value @@ -42,11 +57,29 @@ internal class DefaultEarnComponent( @Composable override fun Content(bottomSheetState: State, modifier: Modifier) { + val bottomSheet by bottomSheetSlot.subscribeAsState() val state by earnModel.state.collectAsStateWithLifecycle() + EarnContent( state = state, modifier = modifier, ) + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + config: FeedPortfolioRoute, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = when (config) { + is FeedPortfolioRoute.AddToPortfolio -> { + addToPortfolioComponentFactory.create( + context = childByContext(componentContext), + params = AddToPortfolioPreselectedDataComponent.Params( + tokenToAdd = config.tokenToAdd, + callback = earnModel.addToPortfolioCallback, + ), + ) + } } @Serializable diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/EarnTokenWithCurrencyToListItemUMConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/EarnTokenWithCurrencyToListItemUMConverter.kt new file mode 100644 index 0000000000..59cadd9281 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/EarnTokenWithCurrencyToListItemUMConverter.kt @@ -0,0 +1,50 @@ +package com.tangem.features.feed.model.converter + +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.domain.models.earn.EarnRewardType +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.domain.models.earn.EarnType +import com.tangem.features.feed.impl.R +import com.tangem.features.feed.ui.earn.state.EarnListItemUM +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class EarnTokenWithCurrencyToListItemUMConverter( + private val onItemClick: (EarnTokenWithCurrency) -> Unit, +) : Converter { + + private val cryptoCurrencyToIconStateConverter = CryptoCurrencyToIconStateConverter() + + override fun convert(value: EarnTokenWithCurrency): EarnListItemUM { + return EarnListItemUM( + network = TextReference.Str(value.networkName), + symbol = TextReference.Str(value.earnToken.tokenSymbol), + tokenName = TextReference.Str(value.earnToken.tokenName), + currencyIconState = cryptoCurrencyToIconStateConverter.convert(value.cryptoCurrency), + earnValue = when (value.earnToken.rewardType) { + EarnRewardType.APR -> TextReference.Res( + id = R.string.staking_apr_earn_badge, + formatArgs = wrappedList(convertPercent(value.earnToken.apy)), + ) + EarnRewardType.APY -> TextReference.Res( + id = R.string.yield_module_earn_badge, + formatArgs = wrappedList(convertPercent(value.earnToken.apy)), + ) + }, + earnType = when (value.earnToken.type) { + EarnType.STAKING -> TextReference.Res(R.string.common_staking) + EarnType.YIELD -> TextReference.Res(R.string.common_yield_mode) + }, + onItemClick = { onItemClick(value) }, + ) + } + + private fun convertPercent(value: String): TextReference { + val percent = BigDecimal(value).format { percent(withPercentSign = false) } + return TextReference.Str(percent) + } +} \ 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 2717a8708d..17eb499815 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 @@ -1,131 +1,157 @@ package com.tangem.features.feed.model.earn import androidx.compose.runtime.Stable +import arrow.core.Either +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.common.R +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter 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.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.earn.usecase.* +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.earn.EarnNetworks +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.feed.components.earn.DefaultEarnComponent -import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM -import com.tangem.features.feed.ui.earn.state.EarnFilterByNetworkBottomSheetContentUM -import com.tangem.features.feed.ui.earn.state.EarnFilterByTypeBottomSheetContentUM -import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM -import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM -import com.tangem.features.feed.ui.earn.state.EarnListUM -import com.tangem.features.feed.ui.earn.state.EarnUM +import com.tangem.features.feed.components.feed.FeedPortfolioRoute +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent +import com.tangem.features.feed.model.earn.state.EarnStateController +import com.tangem.features.feed.model.earn.state.converter.EarnNetworksConverter +import com.tangem.features.feed.model.earn.state.transformers.* +import com.tangem.features.feed.model.earn.statemanager.EarnListBatchFlowManager +import com.tangem.features.feed.model.earn.statemanager.EarnListStateManager +import com.tangem.features.feed.ui.earn.state.* +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject @Stable @ModelScoped +@Suppress("LongParameterList") internal class EarnModel @Inject constructor( - override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val fetchEarnNetworksUseCase: FetchEarnNetworksUseCase, + private val getEarnNetworksUseCase: GetEarnNetworksUseCase, + private val getEarnTokensBatchFlowUseCase: GetEarnTokensBatchFlowUseCase, + private val getTopEarnTokensUseCase: GetTopEarnTokensUseCase, + private val fetchTopEarnTokensUseCase: FetchTopEarnTokensUseCase, + private val appRouter: AppRouter, + private val stateController: EarnStateController, ) : Model() { private val params = paramsContainer.require() + private val earnNetworks = MutableStateFlow(Either.Right(emptyList())) + private val earnNetworksConverter: EarnNetworksConverter by lazy { EarnNetworksConverter() } + private val earnListConfigProvider = Provider { + createEarnTokensListConfig( + selectedTypeFilter = stateController.value.selectedTypeFilter, + selectedNetworkFilter = stateController.value.selectedNetworkFilter, + earnNetworks = earnNetworks.value, + ) + } - // TODO [REDACTED_TASK_KEY] remove mocks - private val mockNetworks: ImmutableList = persistentListOf( - EarnFilterNetworkUM.AllNetworks( - text = resourceReference(R.string.earn_filter_all_networks), - isSelected = false, - ), - EarnFilterNetworkUM.MyNetworks( - text = resourceReference(R.string.earn_filter_my_networks), - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "ethereum", - text = stringReference("Ethereum"), - symbol = stringReference("ETH"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/ethereum.png", - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "polygon", - text = stringReference("Polygon"), - symbol = stringReference("MATIC"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/matic-network.png", - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "base", - text = stringReference("Base"), - symbol = stringReference("BASE"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/base.png", - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "avalanche", - text = stringReference("Avalance"), - symbol = stringReference("AVAX"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/avalanche-2.png", - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "cardano", - text = stringReference("Cardano"), - symbol = stringReference("ADA"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/cardano.png", - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "bsc", - text = stringReference("Bitcoin Smart Chain"), - symbol = stringReference("BSC"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/binancecoin.png", - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "bitcoin", - text = stringReference("Bitcoin"), - symbol = stringReference("BTC"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/bitcoin.png", - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "solana", - text = stringReference("Solana"), - symbol = stringReference("SOL"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/solana.png", - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "ton", - text = stringReference("Ton"), - symbol = stringReference("TON"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/the-open-network.png", - isSelected = false, - ), - ) + private val batchFlowManager by lazy { + EarnListBatchFlowManager( + getEarnTokensBatchFlowUseCase = getEarnTokensBatchFlowUseCase, + configProvider = earnListConfigProvider, + onItemClick = ::onEarnTokenClick, + modelScope = modelScope, + dispatchers = dispatchers, + ) + } + + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + + val addToPortfolioCallback = object : AddToPortfolioPreselectedDataComponent.Callback { + override fun onDismiss() = bottomSheetNavigation.dismiss() + override fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId) { + bottomSheetNavigation.dismiss() + appRouter.push( + AppRoute.CurrencyDetails( + userWalletId = walletId, + currency = addedToken, + ), + ) + } + } val state: StateFlow - field = MutableStateFlow(createInitialState()) + get() = stateController.uiState - private fun createInitialState(): EarnUM { - val defaultNetworkFilter = mockNetworks.first() - val defaultTypeFilter = EarnFilterTypeUM.All + init { + updateInitialState() + fetchEarnNetworks() + fetchTopEarnTokens() + subscribeOnNetworks() + subscribeOnBatchFlow() + subscribeToMostlyUsed() + } - return EarnUM( - mostlyUsed = EarnListUM.Loading, - bestOpportunities = EarnBestOpportunitiesUM.Loading, - selectedTypeFilter = defaultTypeFilter, - selectedNetworkFilter = defaultNetworkFilter, - filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(defaultTypeFilter), - filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig(defaultNetworkFilter), - onBackClick = params.onBackClick, - onNetworkFilterClick = ::onNetworkFilterClick, - onTypeFilterClick = ::onTypeFilterClick, - ) + private fun subscribeOnBatchFlow() { + combine( + batchFlowManager.uiItems, + batchFlowManager.initialLoadingError, + batchFlowManager.paginationStatus, + ) { items, error, paginationStatus -> + val hasActiveFilters = state.value.selectedTypeFilter != EarnFilterTypeUM.All || + state.value.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks + EarnListStateManager.calculateState( + items = items, + error = error, + paginationStatus = paginationStatus, + hasActiveFilters = hasActiveFilters, + onRetryClick = { batchFlowManager.reload() }, + onLoadMore = { batchFlowManager.loadMore() }, + onClearFiltersClick = ::onClearFiltersClick, + ) + }.onEach { bestOpportunitiesState -> + stateController.update(UpdateBestOpportunitiesStateTransformer(bestOpportunitiesState)) + } + .onStart { batchFlowManager.reload() } + .launchIn(modelScope) + } + + private fun subscribeToMostlyUsed() { + modelScope.launch(dispatchers.default) { + getTopEarnTokensUseCase().collect { earnResult -> + stateController.update( + UpdateMostlyUsedStateTransformer( + earnResult = earnResult, + onItemClick = ::onEarnTokenClick, + onRetryClick = ::fetchTopEarnTokens, + ), + ) + } + } + } + + private fun subscribeOnNetworks() { + modelScope.launch(dispatchers.default) { + getEarnNetworksUseCase().collect(earnNetworks) + } + } + + private fun fetchTopEarnTokens() { + modelScope.launch(dispatchers.default) { + fetchTopEarnTokensUseCase() + } + } + + private fun fetchEarnNetworks() { + modelScope.launch(dispatchers.default) { + fetchEarnNetworksUseCase() + } } private fun createTypeFilterBottomSheetConfig( @@ -137,7 +163,7 @@ internal class EarnModel @Inject constructor( onDismissRequest = ::hideTypeFilterBottomSheet, content = EarnFilterByTypeBottomSheetContentUM( selectedOption = selectedOption, - onOptionClick = ::onTypeFilterOptionClick, + onOptionClick = ::onTypeFilterOptionSelected, ), ) } @@ -146,16 +172,40 @@ internal class EarnModel @Inject constructor( selectedNetwork: EarnFilterNetworkUM, isShown: Boolean = false, ): TangemBottomSheetConfig { - val updatedNetworks = mockNetworks.map { network -> - when { - network is EarnFilterNetworkUM.AllNetworks && selectedNetwork is EarnFilterNetworkUM.AllNetworks -> - network.copy(isSelected = true) - network is EarnFilterNetworkUM.MyNetworks && selectedNetwork is EarnFilterNetworkUM.MyNetworks -> - network.copy(isSelected = true) - network is EarnFilterNetworkUM.Network && selectedNetwork is EarnFilterNetworkUM.Network && - network.id == selectedNetwork.id -> - network.copy(isSelected = true) - else -> network + val filters = buildList { + add( + EarnFilterNetworkUM.AllNetworks( + text = TextReference.Res(R.string.earn_filter_all_networks), + isSelected = false, + ), + ) + add( + EarnFilterNetworkUM.MyNetworks( + text = TextReference.Res(R.string.earn_filter_my_networks), + isSelected = false, + ), + ) + earnNetworks.value.onRight { networks -> + addAll(networks.map(earnNetworksConverter::convert)) + } + } + + val updatedNetworks = filters.map { filter -> + val shouldBeSelected = when (filter) { + is EarnFilterNetworkUM.AllNetworks -> selectedNetwork is EarnFilterNetworkUM.AllNetworks + is EarnFilterNetworkUM.MyNetworks -> selectedNetwork is EarnFilterNetworkUM.MyNetworks + is EarnFilterNetworkUM.Network -> + selectedNetwork is EarnFilterNetworkUM.Network && filter.id == selectedNetwork.id + } + + if (filter.isSelected != shouldBeSelected) { + when (filter) { + is EarnFilterNetworkUM.AllNetworks -> filter.copy(isSelected = shouldBeSelected) + is EarnFilterNetworkUM.MyNetworks -> filter.copy(isSelected = shouldBeSelected) + is EarnFilterNetworkUM.Network -> filter.copy(isSelected = shouldBeSelected) + } + } else { + filter } } @@ -165,81 +215,108 @@ internal class EarnModel @Inject constructor( content = EarnFilterByNetworkBottomSheetContentUM( selectedNetwork = selectedNetwork, networks = persistentListOf(*updatedNetworks.toTypedArray()), - onOptionClick = ::onNetworkFilterOptionClick, + onOptionClick = ::onNetworkFilterOptionSelected, ), ) } + /* start of clicks area */ private fun onTypeFilterClick() { - state.update { currentState -> - currentState.copy( - filterByTypeBottomSheet = createTypeFilterBottomSheetConfig( - selectedOption = currentState.selectedTypeFilter, - isShown = true, - ), - ) - } - } - - private fun onNetworkFilterClick() { - state.update { currentState -> - currentState.copy( - filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig( - selectedNetwork = currentState.selectedNetworkFilter, - isShown = true, - ), - ) - } + stateController.update( + EarnTypeFilterStateTransformer( + shouldShow = true, + filterByTypeBottomSheetConfig = ::createTypeFilterBottomSheetConfig, + ), + ) } private fun hideTypeFilterBottomSheet() { - state.update { currentState -> - currentState.copy( - filterByTypeBottomSheet = currentState.filterByTypeBottomSheet.copy(isShown = false), - ) - } + stateController.update( + EarnTypeFilterStateTransformer( + shouldShow = false, + filterByTypeBottomSheetConfig = ::createTypeFilterBottomSheetConfig, + ), + ) + } + + private fun onNetworkFilterClick() { + stateController.update( + EarnNetworkFilterStateTransformer( + shouldShow = true, + filterByNetworkBottomSheetConfig = ::createNetworkFilterBottomSheetConfig, + ), + ) } private fun hideNetworkFilterBottomSheet() { - state.update { currentState -> - currentState.copy( - filterByNetworkBottomSheet = currentState.filterByNetworkBottomSheet.copy(isShown = false), - ) - } + stateController.update( + EarnNetworkFilterStateTransformer( + shouldShow = false, + filterByNetworkBottomSheetConfig = ::createNetworkFilterBottomSheetConfig, + ), + ) } - private fun onTypeFilterOptionClick(type: EarnFilterTypeUM) { - state.update { currentState -> - currentState.copy( - selectedTypeFilter = type, - filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(selectedOption = type), - ) - } + private fun onTypeFilterOptionSelected(type: EarnFilterTypeUM) { + stateController.update( + EarnTypeFilterSelectedStateTransformer( + filter = type, + filterByTypeBottomSheetConfig = ::createTypeFilterBottomSheetConfig, + ), + ) + batchFlowManager.reload() } - private fun onNetworkFilterOptionClick(network: EarnFilterNetworkUM) { - state.update { currentState -> - currentState.copy( - selectedNetworkFilter = network, - filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig(selectedNetwork = network), - ) - } + private fun onNetworkFilterOptionSelected(network: EarnFilterNetworkUM) { + stateController.update( + EarnNetworkFilterSelectedStateTransformer( + filter = network, + filterByNetworkBottomSheetConfig = ::createNetworkFilterBottomSheetConfig, + ), + ) + batchFlowManager.reload() } - // TODO [REDACTED_TASK_KEY] remove - @Suppress("UnusedPrivateMember") private fun onClearFiltersClick() { - val defaultNetworkFilter = mockNetworks.first() - val defaultTypeFilter = EarnFilterTypeUM.All - state.update { currentState -> - currentState.copy( - selectedTypeFilter = defaultTypeFilter, - selectedNetworkFilter = defaultNetworkFilter, - filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(selectedOption = defaultTypeFilter), - filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig( - selectedNetwork = defaultNetworkFilter, + stateController.update( + ClearEarnUMFilterStateTransformer( + filterByTypeBottomSheetConfig = ::createTypeFilterBottomSheetConfig, + filterByNetworkBottomSheetConfig = ::createNetworkFilterBottomSheetConfig, + ), + ) + batchFlowManager.reload() + } + + private fun onEarnTokenClick(earnTokenWithCurrency: EarnTokenWithCurrency) { + bottomSheetNavigation.activate( + FeedPortfolioRoute.AddToPortfolio( + tokenToAdd = AddToPortfolioPreselectedDataComponent.TokenToAdd( + network = TokenMarketInfo.Network( + networkId = earnTokenWithCurrency.earnToken.networkId, + isExchangeable = false, + contractAddress = earnTokenWithCurrency.earnToken.tokenAddress, + decimalCount = earnTokenWithCurrency.earnToken.decimalCount, + ), + id = CryptoCurrency.RawID(earnTokenWithCurrency.earnToken.tokenId), + name = earnTokenWithCurrency.earnToken.tokenName, + symbol = earnTokenWithCurrency.earnToken.tokenSymbol, ), - ) - } + ), + ) + } + /* end of clicks area */ + + private fun updateInitialState() { + stateController.update( + UpdateEarnUMInitialStateTransformer( + onBackClick = params.onBackClick, + onNetworkFilterClick = ::onNetworkFilterClick, + filterByTypeBottomSheetConfig = createTypeFilterBottomSheetConfig(state.value.selectedTypeFilter), + filterByNetworkBottomSheetConfig = createNetworkFilterBottomSheetConfig( + selectedNetwork = state.value.selectedNetworkFilter, + ), + onTypeFilterClick = ::onTypeFilterClick, + ), + ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt new file mode 100644 index 0000000000..08845ff277 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt @@ -0,0 +1,37 @@ +package com.tangem.features.feed.model.earn + +import com.tangem.domain.earn.model.EarnTokensListConfig +import com.tangem.domain.models.earn.EarnNetwork +import com.tangem.domain.models.earn.EarnNetworks +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM + +internal fun createEarnTokensListConfig( + selectedTypeFilter: EarnFilterTypeUM, + selectedNetworkFilter: EarnFilterNetworkUM, + earnNetworks: EarnNetworks, + isForEarn: Boolean = false, +): EarnTokensListConfig { + val type = when (selectedTypeFilter) { + EarnFilterTypeUM.All -> null + EarnFilterTypeUM.Staking -> "staking" + EarnFilterTypeUM.YieldMode -> "yield" + } + val networks = when (selectedNetworkFilter) { + is EarnFilterNetworkUM.AllNetworks -> null + is EarnFilterNetworkUM.MyNetworks -> { + earnNetworks.fold( + ifLeft = { null }, + ifRight = { networks -> + networks.filter(EarnNetwork::isAdded).map(EarnNetwork::networkId) + }, + ) + } + is EarnFilterNetworkUM.Network -> listOf(selectedNetworkFilter.id) + } + return EarnTokensListConfig( + type = type, + networks = networks, + isForEarn = isForEarn, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt new file mode 100644 index 0000000000..6709151fb5 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt @@ -0,0 +1,48 @@ +package com.tangem.features.feed.model.earn.state + +import com.tangem.common.R +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +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 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 EarnStateController @Inject constructor() { + + private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState()) + + val uiState: StateFlow get() = mutableUiState.asStateFlow() + + val value: EarnUM get() = uiState.value + + fun update(function: (EarnUM) -> EarnUM) { + mutableUiState.update(function = function) + } + + fun update(transformer: EarnUMTransformer) { + mutableUiState.update(function = transformer::transform) + } + + private fun getInitialState(): EarnUM { + return EarnUM( + mostlyUsed = EarnListUM.Loading, + bestOpportunities = EarnBestOpportunitiesUM.Loading, + selectedTypeFilter = EarnFilterTypeUM.All, + selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks( + text = TextReference.Res(R.string.earn_filter_all_networks), + isSelected = true, + ), + filterByTypeBottomSheet = TangemBottomSheetConfig.Empty, + filterByNetworkBottomSheet = TangemBottomSheetConfig.Empty, + onBackClick = {}, + onNetworkFilterClick = {}, + onTypeFilterClick = {}, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/converter/EarnNetworksConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/converter/EarnNetworksConverter.kt new file mode 100644 index 0000000000..3f50e4aef8 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/converter/EarnNetworksConverter.kt @@ -0,0 +1,22 @@ +package com.tangem.features.feed.model.earn.state.converter + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.domain.models.earn.EarnNetwork +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.utils.converter.Converter + +internal class EarnNetworksConverter : Converter { + + override fun convert(value: EarnNetwork): EarnFilterNetworkUM.Network { + return EarnFilterNetworkUM.Network( + id = value.networkId, + text = TextReference.Str(value.fullName), + symbol = TextReference.Str(value.symbol), + iconRes = getActiveIconRes(Blockchain.fromNetworkId(value.networkId)?.id.orEmpty()), + isSelected = false, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/ClearEarnUMFilterStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/ClearEarnUMFilterStateTransformer.kt new file mode 100644 index 0000000000..b747c62f70 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/ClearEarnUMFilterStateTransformer.kt @@ -0,0 +1,28 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.common.R +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class ClearEarnUMFilterStateTransformer( + private val filterByTypeBottomSheetConfig: (EarnFilterTypeUM, Boolean) -> TangemBottomSheetConfig, + private val filterByNetworkBottomSheetConfig: (EarnFilterNetworkUM, Boolean) -> TangemBottomSheetConfig, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + val defaultNetworksFilter = EarnFilterNetworkUM.AllNetworks( + text = TextReference.Res(R.string.earn_filter_all_networks), + isSelected = true, + ) + + return prevState.copy( + selectedTypeFilter = EarnFilterTypeUM.All, + selectedNetworkFilter = defaultNetworksFilter, + filterByTypeBottomSheet = filterByTypeBottomSheetConfig(EarnFilterTypeUM.All, false), + filterByNetworkBottomSheet = filterByNetworkBottomSheetConfig(defaultNetworksFilter, false), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt new file mode 100644 index 0000000000..a9ee578d30 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class EarnNetworkFilterSelectedStateTransformer( + private val filter: EarnFilterNetworkUM, + private val filterByNetworkBottomSheetConfig: (EarnFilterNetworkUM, Boolean) -> TangemBottomSheetConfig, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return prevState.copy( + selectedNetworkFilter = filter, + filterByNetworkBottomSheet = filterByNetworkBottomSheetConfig(filter, false), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterStateTransformer.kt new file mode 100644 index 0000000000..5594b46a70 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterStateTransformer.kt @@ -0,0 +1,21 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class EarnNetworkFilterStateTransformer( + private val shouldShow: Boolean, + private val filterByNetworkBottomSheetConfig: (EarnFilterNetworkUM, Boolean) -> TangemBottomSheetConfig, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return if (shouldShow) { + prevState.copy( + filterByNetworkBottomSheet = filterByNetworkBottomSheetConfig(prevState.selectedNetworkFilter, true), + ) + } else { + prevState.copy(filterByNetworkBottomSheet = prevState.filterByNetworkBottomSheet.copy(isShown = false)) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterSelectedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterSelectedStateTransformer.kt new file mode 100644 index 0000000000..3110ddfbfc --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterSelectedStateTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class EarnTypeFilterSelectedStateTransformer( + private val filter: EarnFilterTypeUM, + private val filterByTypeBottomSheetConfig: (EarnFilterTypeUM, Boolean) -> TangemBottomSheetConfig, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return prevState.copy( + selectedTypeFilter = filter, + filterByTypeBottomSheet = filterByTypeBottomSheetConfig(filter, false), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterStateTransformer.kt new file mode 100644 index 0000000000..dacdb66034 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterStateTransformer.kt @@ -0,0 +1,21 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class EarnTypeFilterStateTransformer( + private val shouldShow: Boolean, + private val filterByTypeBottomSheetConfig: (EarnFilterTypeUM, Boolean) -> TangemBottomSheetConfig, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return if (shouldShow) { + prevState.copy( + filterByTypeBottomSheet = filterByTypeBottomSheetConfig(prevState.selectedTypeFilter, true), + ) + } else { + prevState.copy(filterByTypeBottomSheet = prevState.filterByTypeBottomSheet.copy(isShown = false)) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnUMTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnUMTransformer.kt new file mode 100644 index 0000000000..0cc4c5b3c3 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnUMTransformer.kt @@ -0,0 +1,7 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal interface EarnUMTransformer { + fun transform(prevState: EarnUM): EarnUM +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateBestOpportunitiesStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateBestOpportunitiesStateTransformer.kt new file mode 100644 index 0000000000..d460f89513 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateBestOpportunitiesStateTransformer.kt @@ -0,0 +1,13 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class UpdateBestOpportunitiesStateTransformer( + private val newState: EarnBestOpportunitiesUM, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return prevState.copy(bestOpportunities = newState) + } +} \ 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 new file mode 100644 index 0000000000..c726dfd6af --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class UpdateEarnUMInitialStateTransformer( + private val onBackClick: () -> Unit, + private val filterByTypeBottomSheetConfig: TangemBottomSheetConfig, + private val filterByNetworkBottomSheetConfig: TangemBottomSheetConfig, + private val onNetworkFilterClick: () -> Unit, + private val onTypeFilterClick: () -> Unit, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return prevState.copy( + onBackClick = onBackClick, + filterByNetworkBottomSheet = filterByNetworkBottomSheetConfig, + filterByTypeBottomSheet = filterByTypeBottomSheetConfig, + onNetworkFilterClick = onNetworkFilterClick, + onTypeFilterClick = onTypeFilterClick, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt new file mode 100644 index 0000000000..9f280044d9 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt @@ -0,0 +1,38 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.domain.models.earn.EarnTopToken +import com.tangem.features.feed.model.converter.EarnTokenWithCurrencyToListItemUMConverter +import com.tangem.features.feed.ui.earn.state.EarnListUM +import com.tangem.features.feed.ui.earn.state.EarnUM +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +internal class UpdateMostlyUsedStateTransformer( + private val earnResult: EarnTopToken?, + private val onItemClick: (EarnTokenWithCurrency) -> Unit, + private val onRetryClick: () -> Unit, +) : EarnUMTransformer { + + private val converter = EarnTokenWithCurrencyToListItemUMConverter(onItemClick = onItemClick) + + override fun transform(prevState: EarnUM): EarnUM { + return when (earnResult) { + null -> prevState.copy(mostlyUsed = EarnListUM.Loading) + else -> earnResult.fold( + ifLeft = { prevState.copy(mostlyUsed = EarnListUM.Error(onRetryClicked = onRetryClick)) }, + ifRight = { list -> + val newItems = list + .sortedWith( + compareByDescending { + it.earnToken.apy.toBigDecimalOrNull() ?: BigDecimal.ZERO + }.thenBy { it.earnToken.tokenName }, + ) + .map(converter::convert) + .toPersistentList() + prevState.copy(mostlyUsed = EarnListUM.Content(items = newItems)) + }, + ) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt new file mode 100644 index 0000000000..685771c34d --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt @@ -0,0 +1,105 @@ +package com.tangem.features.feed.model.earn.statemanager + +import com.tangem.domain.earn.model.EarnTokensBatchingContext +import com.tangem.domain.earn.model.EarnTokensListConfig +import com.tangem.domain.earn.usecase.GetEarnTokensBatchFlowUseCase +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.features.feed.model.converter.EarnTokenWithCurrencyToListItemUMConverter +import com.tangem.features.feed.ui.earn.state.EarnListItemUM +import com.tangem.pagination.BatchAction +import com.tangem.pagination.PaginationStatus +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch + +internal class EarnListBatchFlowManager( + getEarnTokensBatchFlowUseCase: GetEarnTokensBatchFlowUseCase, + private val configProvider: Provider, + private val onItemClick: (EarnTokenWithCurrency) -> Unit, + private val modelScope: CoroutineScope, + private val dispatchers: CoroutineDispatcherProvider, +) { + private val actionsFlow = MutableSharedFlow>() + private val converter = EarnTokenWithCurrencyToListItemUMConverter(onItemClick = onItemClick) + + private val batchFlow = getEarnTokensBatchFlowUseCase( + context = EarnTokensBatchingContext( + actionsFlow = actionsFlow, + coroutineScope = modelScope, + ), + batchSize = DEFAULT_BATCH_SIZE, + ) + + val uiItems: StateFlow> = + batchFlow.state + .scan(persistentListOf() to -1) { (accItems, lastProcessedBatchIndex), newState -> + if (newState.data.size <= lastProcessedBatchIndex) { + val newItems = newState.data + .flatMap { it.data } + .map(converter::convert) + .toPersistentList() + newItems to newState.data.lastIndex + } else { + val newBatches = newState.data.subList(lastProcessedBatchIndex + 1, newState.data.size) + val newItems = newBatches + .flatMap { it.data } + .map(converter::convert) + accItems.addAll(newItems) to newState.data.lastIndex + } + } + .map { (items, _) -> items } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = persistentListOf(), + ) + + val initialLoadingError: StateFlow = batchFlow.state + .map { state -> + val status = state.status + if (status is PaginationStatus.InitialLoadingError) { + status.throwable + } else { + null + } + } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + + val paginationStatus: StateFlow>> = batchFlow.state + .map { it.status } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = PaginationStatus.InitialLoading, + ) + + fun reload() { + modelScope.launch(dispatchers.default) { + actionsFlow.emit( + BatchAction.Reload(requestParams = configProvider()), + ) + } + } + + fun loadMore() { + modelScope.launch(dispatchers.default) { + actionsFlow.emit(BatchAction.LoadMore()) + } + } + + private companion object { + private const val DEFAULT_BATCH_SIZE = 20 + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListStateManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListStateManager.kt new file mode 100644 index 0000000000..4b53e07f06 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListStateManager.kt @@ -0,0 +1,28 @@ +package com.tangem.features.feed.model.earn.statemanager + +import com.tangem.pagination.PaginationStatus +import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM +import com.tangem.features.feed.ui.earn.state.EarnListItemUM +import kotlinx.collections.immutable.ImmutableList + +@Suppress("LongParameterList") +internal object EarnListStateManager { + + fun calculateState( + items: ImmutableList, + error: Throwable?, + paginationStatus: PaginationStatus<*>, + hasActiveFilters: Boolean, + onRetryClick: () -> Unit, + onLoadMore: () -> Unit, + onClearFiltersClick: () -> Unit, + ): EarnBestOpportunitiesUM = when { + error != null -> EarnBestOpportunitiesUM.Error(onRetryClicked = onRetryClick) + paginationStatus is PaginationStatus.InitialLoading && items.isEmpty() -> + EarnBestOpportunitiesUM.Loading + items.isEmpty() && hasActiveFilters -> + EarnBestOpportunitiesUM.EmptyFiltered(onClearFilterClick = onClearFiltersClick) + items.isEmpty() -> EarnBestOpportunitiesUM.Empty + else -> EarnBestOpportunitiesUM.Content(items = items, onLoadMore = onLoadMore) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt index bf3daee21c..3d4594e1eb 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt @@ -1,19 +1,10 @@ package com.tangem.features.feed.model.feed.state.transformers -import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.domain.models.earn.EarnRewardType import com.tangem.domain.models.earn.EarnTokenWithCurrency import com.tangem.domain.models.earn.EarnTopToken -import com.tangem.domain.models.earn.EarnType -import com.tangem.features.feed.impl.R -import com.tangem.features.feed.ui.earn.state.EarnListItemUM +import com.tangem.features.feed.model.converter.EarnTokenWithCurrencyToListItemUMConverter import com.tangem.features.feed.ui.earn.state.EarnListUM import com.tangem.features.feed.ui.feed.state.FeedListUM -import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal @@ -24,7 +15,7 @@ internal class UpdateEarnStateTransformer( private val earnResult: EarnTopToken?, ) : FeedListUMTransformer { - private val earnTokenWithCurrencyConverter = EarnTokenWithCurrencyConverter(onItemClick = onItemClick) + private val earnTokenWithCurrencyConverter = EarnTokenWithCurrencyToListItemUMConverter(onItemClick = onItemClick) override fun transform(prevState: FeedListUM): FeedListUM { if (!isEarnEnabled) return handleEmptyState(prevState) @@ -60,52 +51,22 @@ internal class UpdateEarnStateTransformer( currentState: FeedListUM, earnTokensWithCurrency: List, ): FeedListUM { + val newItems = earnTokensWithCurrency + .sortedWith( + compareByDescending { + it.earnToken.apy.toBigDecimalOrNull() ?: BigDecimal.ZERO + }.thenBy { it.earnToken.tokenName }, + ) + .map(earnTokenWithCurrencyConverter::convert) + .toPersistentList() + + val newEarnListUM = when (val earnState = currentState.earnListUM) { + is EarnListUM.Content -> earnState.copy(items = newItems) + else -> EarnListUM.Content(items = newItems) + } + return currentState.copy( - earnListUM = EarnListUM.Content( - items = earnTokensWithCurrency - .sortedWith( - compareByDescending { - it.earnToken.apy.toBigDecimalOrNull() ?: BigDecimal.ZERO - }.thenBy { it.earnToken.tokenName }, - ) - .map { earnToken -> - earnTokenWithCurrencyConverter.convert(earnToken) - } - .toPersistentList(), - ), + earnListUM = newEarnListUM, ) } - - private class EarnTokenWithCurrencyConverter( - private val onItemClick: (EarnTokenWithCurrency) -> Unit, - ) : Converter { - override fun convert(value: EarnTokenWithCurrency): EarnListItemUM { - return EarnListItemUM( - network = TextReference.Str(value.networkName), - symbol = TextReference.Str(value.earnToken.tokenSymbol), - tokenName = TextReference.Str(value.earnToken.tokenName), - currencyIconState = CryptoCurrencyToIconStateConverter().convert(value.cryptoCurrency), - earnValue = when (value.earnToken.rewardType) { - EarnRewardType.APR -> TextReference.Res( - id = R.string.staking_apr_earn_badge, - formatArgs = wrappedList(convertPercent(value.earnToken.apy)), - ) - EarnRewardType.APY -> TextReference.Res( - id = R.string.yield_module_earn_badge, - formatArgs = wrappedList(convertPercent(value.earnToken.apy)), - ) - }, - earnType = when (value.earnToken.type) { - EarnType.STAKING -> TextReference.Res(R.string.common_staking) - EarnType.YIELD -> TextReference.Res(R.string.common_yield_mode) - }, - onItemClick = { onItemClick(value) }, - ) - } - - private fun convertPercent(value: String): TextReference { - val percent = BigDecimal(value).format { percent(withPercentSign = false) } - return TextReference.Str(percent) - } - } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt index fa5cff5b5e..6b9886c03f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt @@ -15,6 +15,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -55,13 +56,22 @@ internal open class NewsListBatchFlowManager( initialValue = persistentListOf(), ) - val uiItems: StateFlow> - get() = batchFlow.state - .map { batchListState -> - batchListState.data - .flatMap { batch -> batch.data } - .let { articles -> converter.convert(articles) } + val uiItems: StateFlow> = + batchFlow.state + .scan(persistentListOf() to -1) { (accItems, lastProcessedBatchIndex), newState -> + if (newState.data.size <= lastProcessedBatchIndex) { + val newItems = converter.convert(newState.data.flatMap { it.data }) + .toPersistentList() + + newItems to newState.data.lastIndex + } else { + val newBatches = newState.data.subList(lastProcessedBatchIndex + 1, newState.data.size) + val newShortArticles = newBatches.flatMap { it.data } + val newItems = converter.convert(newShortArticles) + accItems.addAll(newItems) to newState.data.lastIndex + } } + .map { (items, _) -> items } .distinctUntilChanged() .stateIn( scope = modelScope, 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 63753d95a1..32f85c07b6 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 @@ -5,14 +5,13 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.* import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -20,53 +19,49 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.R -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SmallButtonShimmer -import com.tangem.core.ui.components.SpacerW -import com.tangem.core.ui.components.SpacerWMax -import com.tangem.core.ui.components.UnableToLoadData +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.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.components.list.InfiniteListHandler import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.features.feed.ui.earn.components.EarnFilterByNetworkBottomSheet -import com.tangem.features.feed.ui.earn.components.EarnFilterByTypeBottomSheet -import com.tangem.features.feed.ui.earn.components.EarnItemPlaceholder -import com.tangem.features.feed.ui.earn.components.EarnListItem -import com.tangem.features.feed.ui.earn.components.MostlyUsedPlaceholder -import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM -import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM -import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM -import com.tangem.features.feed.ui.earn.state.EarnListItemUM -import com.tangem.features.feed.ui.earn.state.EarnListUM -import com.tangem.features.feed.ui.earn.state.EarnUM +import com.tangem.features.feed.ui.earn.components.* +import com.tangem.features.feed.ui.earn.state.* import kotlinx.collections.immutable.persistentListOf +private const val EARN_LOAD_MORE_BUFFER = 3 + @Composable internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { val background = LocalMainBottomSheetColor.current.value val density = LocalDensity.current val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() } + val listState = rememberLazyListState() EarnFilterByTypeBottomSheet(config = state.filterByTypeBottomSheet) EarnFilterByNetworkBottomSheet(config = state.filterByNetworkBottomSheet) + if (state.bestOpportunities is EarnBestOpportunitiesUM.Content) { + PaginationHandler( + listState = listState, + state = state.bestOpportunities, + ) + } + LazyColumn( + state = listState, modifier = modifier .fillMaxSize() .background(background), @@ -109,7 +104,10 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { @Composable private fun MostlyUsedContent(state: EarnListUM) { - AnimatedContent(state) { st -> + AnimatedContent( + targetState = state, + contentKey = { it::class.java }, + ) { st -> when (st) { is EarnListUM.Loading -> { MostlyUsedPlaceholder() @@ -174,9 +172,11 @@ private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: verticalAlignment = Alignment.CenterVertically, ) { Text( + modifier = Modifier.weight(weight = 1f, fill = false), text = item.tokenName.resolveReference(), color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle2, + overflow = TextOverflow.Ellipsis, maxLines = 1, ) SpacerW(4.dp) @@ -207,34 +207,15 @@ private fun BestOpportunitiesFilters( onNetworkFilterClick: () -> Unit, onTypeFilterClick: () -> Unit, ) { - AnimatedContent(state) { st -> - when (st) { - is EarnBestOpportunitiesUM.Loading -> { - FilterButtonsShimmer() - } - is EarnBestOpportunitiesUM.EmptyFiltered, - is EarnBestOpportunitiesUM.Content, - -> { - FilterButtons( - selectedNetworkFilterText = selectedNetworkFilterText, - selectedTypeFilterText = selectedTypeFilterText, - isEnabled = true, - onNetworkFilterClick = onNetworkFilterClick, - onTypeFilterClick = onTypeFilterClick, - ) - } - is EarnBestOpportunitiesUM.Empty, - is EarnBestOpportunitiesUM.Error, - -> { - FilterButtons( - selectedNetworkFilterText = selectedNetworkFilterText, - selectedTypeFilterText = selectedTypeFilterText, - isEnabled = false, - onNetworkFilterClick = onNetworkFilterClick, - onTypeFilterClick = onTypeFilterClick, - ) - } - } + when (state) { + is EarnBestOpportunitiesUM.Loading -> FilterButtonsShimmer() + else -> FilterButtons( + selectedNetworkFilterText = selectedNetworkFilterText, + selectedTypeFilterText = selectedTypeFilterText, + isEnabled = state is EarnBestOpportunitiesUM.Content || state is EarnBestOpportunitiesUM.EmptyFiltered, + onNetworkFilterClick = onNetworkFilterClick, + onTypeFilterClick = onTypeFilterClick, + ) } } @@ -424,13 +405,28 @@ private fun SectionHeader(title: String, modifier: Modifier = Modifier) { Text( modifier = modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .padding(start = 20.dp, end = 16.dp), text = title, style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, ) } +@Composable +private fun PaginationHandler(listState: LazyListState, state: EarnBestOpportunitiesUM.Content) { + InfiniteListHandler( + listState = listState, + buffer = EARN_LOAD_MORE_BUFFER, + triggerLoadMoreCheckOnItemsCountChange = true, + onLoadMore = remember(state) { + { + state.onLoadMore() + true + } + }, + ) +} + @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -465,6 +461,7 @@ private fun EarnContentPreview() { network = "Ethereum Network", ), ), + onLoadMore = {}, ), ), ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt index 6b31102b92..a04546676b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt @@ -1,6 +1,7 @@ package com.tangem.features.feed.ui.earn.components import android.content.res.Configuration +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -13,14 +14,15 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.currency.icon.CoinIcon import com.tangem.core.ui.components.inputrow.InputRowChecked import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.components.rows.RowContentContainer @@ -165,12 +167,10 @@ private fun LazyListScope.specificNetworksList( .heightIn(52.dp) .padding(horizontal = 12.dp), icon = { - CoinIcon( + Image( modifier = Modifier.size(22.dp), - url = item.iconUrl, - alpha = 1f, - colorFilter = null, - fallbackResId = R.drawable.ic_custom_token_44, + imageVector = ImageVector.vectorResource(item.iconRes), + contentDescription = item.symbol.resolveReference(), ) }, text = { @@ -225,14 +225,14 @@ private fun Preview() { id = "ethereum", text = stringReference("Ethereum"), symbol = stringReference("ETH"), - iconUrl = null, + iconRes = R.drawable.img_btc_22, isSelected = false, ), EarnFilterNetworkUM.Network( id = "polygon", text = stringReference("Polygon"), symbol = stringReference("MATIC"), - iconUrl = null, + iconRes = R.drawable.img_btc_22, isSelected = false, ), ), diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt index 1fd414edf4..bb22ced386 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt @@ -23,7 +23,7 @@ internal sealed class EarnFilterNetworkUM { val id: String, override val text: TextReference, val symbol: TextReference, - val iconUrl: String?, + val iconRes: Int, override val isSelected: Boolean, ) : EarnFilterNetworkUM() } \ No newline at end of file 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 358cab5ee9..c4506586bc 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 @@ -17,7 +17,10 @@ internal sealed interface EarnBestOpportunitiesUM { data object Loading : EarnBestOpportunitiesUM data object Empty : EarnBestOpportunitiesUM data class EmptyFiltered(val onClearFilterClick: () -> Unit) : EarnBestOpportunitiesUM - data class Content(val items: ImmutableList) : EarnBestOpportunitiesUM + data class Content( + val items: ImmutableList, + val onLoadMore: () -> Unit, + ) : EarnBestOpportunitiesUM data class Error(val onRetryClicked: () -> Unit) : EarnBestOpportunitiesUM } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt index 129ef7438d..fb70686941 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt @@ -45,10 +45,13 @@ internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modif modifier = Modifier.padding(horizontal = 16.dp), colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), ) { - AnimatedContent(targetState = earnListUM) { earnListState -> - when (earnListState) { - is EarnListUM.Content -> EarnContentBlock(items = earnListState.items) - is EarnListUM.Error -> EarnErrorBlock(onRetryClick = earnListState.onRetryClicked) + AnimatedContent( + targetState = earnListUM, + contentKey = { it::class.java }, + ) { earnListUM -> + when (earnListUM) { + is EarnListUM.Content -> EarnContentBlock(items = earnListUM.items) + is EarnListUM.Error -> EarnErrorBlock(onRetryClick = earnListUM.onRetryClicked) EarnListUM.Loading -> EarnListPlaceholder(placeholderCount = PLACEHOLDER_ITEM_COUNT) } } diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt index 3dd7bc3947..70f0fe63d0 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt @@ -52,9 +52,7 @@ internal class DefaultTokenReceiveComponent @AssistedInject constructor( ) } - override fun dismiss() { - model.params.onDismiss() - } + override fun dismiss() = model.params.onDismiss() private fun onChildBack() { when (contentStack.value.active.configuration) { From 498e73d6df04724215832b01a401f8347cab0473 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Feb 2026 15:12:05 +0400 Subject: [PATCH 032/106] Updated on 2026-08-14 --- .../api/express/TangemExpressApi.kt | 1 + .../models/response/ExchangeProvider.kt | 3 ++ .../converter/ExpressProviderConverter.kt | 1 + .../data/swap/DefaultSwapRepositoryV2.kt | 2 ++ .../domain/express/models/ExpressProvider.kt | 3 ++ .../tangem/domain/swap/SwapRepositoryV2.kt | 1 + .../domain/swap/usecase/GetSwapDataUseCase.kt | 2 ++ .../usecase/GetSwapSupportedPairsUseCase.kt | 16 +++++++--- .../swap/v2/impl/common/ConfirmData.kt | 1 + .../confirm/model/SendWithSwapConfirmModel.kt | 1 + .../confirm/model/SwapTransactionSender.kt | 1 + .../success/ui/SendWithSwapSuccessContent.kt | 31 +++++++++++++++++-- .../feature/swap/DefaultSwapRepository.kt | 2 ++ .../swap/converters/SwapPairInfoConverter.kt | 2 ++ .../feature/swap/domain/api/SwapRepository.kt | 1 + .../domain/models/domain/SwapPairLeast.kt | 2 ++ 16 files changed, 63 insertions(+), 7 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 3a4c3d9e84..d4e2600da3 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 @@ -67,6 +67,7 @@ interface TangemExpressApi { @Query("refundAddress") refundAddress: String?, // for cex only @Query("refundExtraId") refundExtraId: String?, // for cex only @Query("partnerOperationType") partnerOperationType: String?, // swap/ swap-and-send + @Query("toExtraId") toExtraId: String?, // swap-and-send memo ): ApiResponse @GET("exchange-status") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt index 4712dadadc..a8ed2fd5dc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt @@ -35,6 +35,9 @@ data class ExchangeProvider( @Json(name = "exchangeOnlyWithinSingleAddress") val isExchangeOnlyWithinSingleAddress: Boolean = false, + + @Json(name = "isExtraIdSupported") + val isExtraIdSupported: Boolean = false, ) @JsonClass(generateAdapter = false) diff --git a/data/express/src/main/java/com/tangem/data/express/converter/ExpressProviderConverter.kt b/data/express/src/main/java/com/tangem/data/express/converter/ExpressProviderConverter.kt index 09a2f55b15..94d78677bb 100644 --- a/data/express/src/main/java/com/tangem/data/express/converter/ExpressProviderConverter.kt +++ b/data/express/src/main/java/com/tangem/data/express/converter/ExpressProviderConverter.kt @@ -20,6 +20,7 @@ internal class ExpressProviderConverter : Converter // Search available to swap currency cryptoCurrencyList.any { currencyStatus -> - // Allowed only on networks without tx extras (e.i. memo and destination tag) - val isExtrasSupported = currencyStatus.network.transactionExtrasType.isTxExtrasSupported() - currencyStatus.id == pair.to.currency.id && !isExtrasSupported + currencyStatus.id == pair.to.currency.id } } - .map { pair -> SwapCryptoCurrency(groupingCurrency(pair), pair.providers) } + .map { pair -> + val toCurrency = groupingCurrency(pair) + val isTxExtrasSupported = toCurrency.currency.network.transactionExtrasType.isTxExtrasSupported() + val filteredProviders = if (isTxExtrasSupported) { + pair.providers.filter { it.isExtraIdSupported } + } else { + pair.providers + } + SwapCryptoCurrency(toCurrency, filteredProviders) + } + .filter { it.providers.isNotEmpty() } .toList() val unavailableCryptoCurrencies = 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 5ce32201dc..b825e3f9e7 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 @@ -13,6 +13,7 @@ internal data class ConfirmData( val reduceAmountBy: BigDecimal, val isIgnoreReduce: Boolean, val enteredDestination: String?, + val enteredMemo: String?, val fee: Fee?, val feeError: GetFeeError?, val fromCryptoCurrencyStatus: CryptoCurrencyStatus?, 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 50666b4e1b..4c22e6b47c 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 @@ -142,6 +142,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( reduceAmountBy = amountState?.reduceAmountBy.takeIf { isQuoteContent }.orZero(), isIgnoreReduce = amountState?.isIgnoreReduce == true, enteredDestination = destinationUM?.addressTextField?.actualAddress, + enteredMemo = destinationUM?.memoTextField?.value, fee = feeSelectorUM?.selectedFeeItem?.fee.takeIf { isQuoteContent }, feeError = (uiState.value.feeSelectorUM as? FeeSelectorUM.Error)?.error.takeIf { isQuoteContent }, fromCryptoCurrencyStatus = amountUM?.swapDirection?.withSwapDirection( 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 50a94cdb36..7a166639ff 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 @@ -104,6 +104,7 @@ internal class SwapTransactionSender @AssistedInject constructor( fromAmount = fromAmount.toStringWithRightOffset(fromStatus.currency.decimals), toCryptoCurrency = toStatus.currency, toAddress = destination, + toExtraId = confirmData.enteredMemo, expressProvider = provider, rateType = rateType, expressOperationType = expressOperationType, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt index 44ca767ac2..5657f00361 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt @@ -125,7 +125,10 @@ private fun SuccessContent(sendWithSwapUM: SendWithSwapUM, modifier: Modifier = .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action), ) - DestinationBlock(destinationUM.addressTextField) + DestinationBlock( + address = destinationUM.addressTextField, + memo = destinationUM.memoTextField, + ) FeeBlock(feeSelectorUM = feeSelectorUM) Spacer(Modifier.height(16.dp)) } @@ -247,7 +250,11 @@ private fun FeeBlock(feeSelectorUM: FeeSelectorUM.Content) { } @Composable -private fun DestinationBlock(address: DestinationTextFieldUM.RecipientAddress, modifier: Modifier = Modifier) { +private fun DestinationBlock( + address: DestinationTextFieldUM.RecipientAddress, + memo: DestinationTextFieldUM.RecipientMemo?, + modifier: Modifier = Modifier, +) { Column( modifier = modifier .fillMaxWidth() @@ -279,6 +286,14 @@ private fun DestinationBlock(address: DestinationTextFieldUM.RecipientAddress, m .background(TangemTheme.colors.background.tertiary), ) } + if (memo != null && memo.value.isNotBlank()) { + Text( + text = stringResourceSafe(R.string.send_memo, memo.value), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) + } } } @@ -304,7 +319,17 @@ private fun SendWithSwapSuccessContent_Preview() { isValuePasted = false, blockchainAddress = "0x391316d97a07027a0702c8A002c8A0C25d8470", ), - memoTextField = null, + memoTextField = DestinationTextFieldUM.RecipientMemo( + value = "123123123", + keyboardOptions = KeyboardOptions(), + placeholder = TextReference.EMPTY, + label = resourceReference(R.string.send_recipient), + isError = false, + error = null, + isValuePasted = false, + isEnabled = true, + disabledText = TextReference.EMPTY, + ), recent = persistentListOf(), wallets = persistentListOf(), networkName = "Polygon", 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 e6be947657..d207e7992b 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 @@ -286,6 +286,7 @@ internal class DefaultSwapRepository( expressOperationType: ExpressOperationType, refundAddress: String?, // for cex only refundExtraId: String?, // for cex only + toExtraId: String?, // for networks with memo only ): Either { return withContext(coroutineDispatcher.io) { try { @@ -311,6 +312,7 @@ internal class DefaultSwapRepository( userWallet = userWallet, appPreferencesStore = appPreferencesStore, ), + toExtraId = toExtraId, ).getOrThrow() if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) { val txDetails = parseTxDetails(response.txDetailsJson) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index a64fc2cf07..1080578a05 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -48,6 +48,7 @@ class SwapPairInfoConverter : Converter // TODO: Add target error handling, remove either ([REDACTED_JIRA]) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt index bf97fe58d9..f65dd60a98 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -53,6 +53,8 @@ data class SwapProvider( val isRecommended: Boolean = false, @Json(name = "slippage") val slippage: BigDecimal?, + @Json(name = "isExtraIdSupported") + val isExtraIdSupported: Boolean = false, ) @JsonClass(generateAdapter = false) From 89fb66aa3e4be342249c3b620a5d3bf62616db6c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Feb 2026 15:13:07 +0400 Subject: [PATCH 033/106] Updated on 2026-08-14 --- features/details/impl/build.gradle.kts | 1 + .../preview/PreviewUserWalletListComponent.kt | 6 ++ .../details/entity/UserWalletListUM.kt | 7 ++ .../details/model/UserWalletListModel.kt | 35 ++++++++ .../details/ui/UserWalletListBlock.kt | 90 ++++++++++++++++--- .../wallet/child/wallet/model/WalletModel.kt | 20 +++++ .../model/WalletsUpdateActionResolver.kt | 19 ++++ .../transformers/ReorderWalletsTransformer.kt | 29 ++++++ 8 files changed, 197 insertions(+), 10 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReorderWalletsTransformer.kt diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index 8da459c047..af8d079514 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -68,6 +68,7 @@ dependencies { implementation(deps.compose.material3) implementation(deps.compose.shimmer) implementation(deps.compose.coil) + implementation(deps.compose.reorderableV2) /* DI */ implementation(deps.hilt.android) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt index 86ecb2a42f..fb299d1548 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.features.details.component.UserWalletListComponent import com.tangem.features.details.entity.UserWalletListUM +import com.tangem.features.details.entity.WalletReorderUM import com.tangem.features.details.impl.R import com.tangem.features.details.ui.UserWalletListBlock import kotlinx.collections.immutable.persistentListOf @@ -51,6 +52,11 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { addNewWalletText = resourceReference(R.string.user_wallet_list_add_button), isWalletSavingInProgress = true, onAddNewWalletClick = {}, + walletReorderUM = WalletReorderUM( + isDragEnabled = true, + onMove = { _, _ -> }, + onDragStopped = {}, + ), ), ) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt index a8ef5eb141..375c63345a 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt @@ -11,4 +11,11 @@ internal data class UserWalletListUM( val isWalletSavingInProgress: Boolean, val addNewWalletText: TextReference, val onAddNewWalletClick: () -> Unit, + val walletReorderUM: WalletReorderUM, +) + +internal data class WalletReorderUM( + val isDragEnabled: Boolean, + val onMove: (fromIndex: Int, toIndex: Int) -> Unit, + val onDragStopped: () -> Unit, ) \ No newline at end of file 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 fbf60da951..5c011e92c8 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 @@ -13,15 +13,19 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.ApplyUserWalletListSortingUseCase import com.tangem.domain.wallets.usecase.UnlockWalletUseCase import com.tangem.features.details.entity.UserWalletListUM +import com.tangem.features.details.entity.WalletReorderUM import com.tangem.features.details.impl.R import com.tangem.features.details.utils.UserWalletSaver import com.tangem.features.hotwallet.HotWalletFeatureToggles +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -38,6 +42,8 @@ internal class UserWalletListModel @Inject constructor( private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val unlockWalletUseCase: UnlockWalletUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + private val walletFeatureToggles: WalletFeatureToggles, + private val applyUserWalletListSortingUseCase: ApplyUserWalletListSortingUseCase, ) : Model() { private val isWalletSavingInProgress: MutableStateFlow = MutableStateFlow(value = false) @@ -55,6 +61,11 @@ internal class UserWalletListModel @Inject constructor( isWalletSavingInProgress = false, addNewWalletText = TextReference.EMPTY, onAddNewWalletClick = ::onAddNewWalletClick, + walletReorderUM = WalletReorderUM( + isDragEnabled = false, + onMove = ::onWalletReorder, + onDragStopped = ::onWalletDragStopped, + ), ), ) @@ -78,6 +89,11 @@ internal class UserWalletListModel @Inject constructor( userWallets = userWallets, isWalletSavingInProgress = isWalletSavingInProgress, addNewWalletText = resourceReference(R.string.user_wallet_list_add_button), + walletReorderUM = WalletReorderUM( + isDragEnabled = walletFeatureToggles.isWalletReorderFeatureEnabled && userWallets.size > 1, + onMove = ::onWalletReorder, + onDragStopped = ::onWalletDragStopped, + ), ) } @@ -109,4 +125,23 @@ internal class UserWalletListModel @Inject constructor( } } } + + private fun onWalletReorder(fromIndex: Int, toIndex: Int) { + state.update { prevState -> + val wallets = prevState.userWallets.toMutableList() + wallets.add(toIndex, wallets.removeAt(fromIndex)) + prevState.copy(userWallets = wallets.toPersistentList()) + } + } + + private fun onWalletDragStopped() { + val userWalletIds = state.value.userWallets.map { UserWalletId(it.id) } + + modelScope.launch { + applyUserWalletListSortingUseCase(userWalletIds) + .onLeft { error -> + Timber.e("Failed to apply wallet list sorting: $error") + } + } + } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index e253ffec97..2cff0e2d16 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -2,17 +2,25 @@ package com.tangem.features.details.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.key +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview @@ -20,6 +28,7 @@ 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.userwallet.UserWalletItem +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference @@ -28,26 +37,84 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.details.component.UserWalletListComponent import com.tangem.features.details.component.preview.PreviewUserWalletListComponent import com.tangem.features.details.entity.UserWalletListUM +import com.tangem.features.details.entity.WalletReorderUM import com.tangem.features.details.impl.R +import sh.calvin.reorderable.ReorderableItem +import sh.calvin.reorderable.ReorderableLazyListState +import sh.calvin.reorderable.rememberReorderableLazyListState @Composable internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = Modifier) { + val listState = rememberLazyListState() + val reorderableListState = rememberReorderableLazyListState( + lazyListState = listState, + onMove = { from, to -> state.walletReorderUM.onMove(from.index, to.index) }, + ) + + val listHeight = WALLET_ITEM_HEIGHT * state.userWallets.size + ADD_WALLET_BUTTON_HEIGHT + BlockCard( modifier = modifier, ) { - state.userWallets.forEach { state -> - key(state.id) { - UserWalletItem( + LazyColumn( + state = listState, + modifier = Modifier + .heightIn(max = listHeight), + ) { + items( + items = state.userWallets, + key = { it.id }, + ) { walletState -> + WalletItem( + model = walletState, + reorderableListState = reorderableListState, + walletReorderUM = state.walletReorderUM, modifier = Modifier.fillMaxWidth(), - state = state, + ) + } + item(key = "add_wallet_button") { + AddWalletButton( + text = state.addNewWalletText, + isInProgress = state.isWalletSavingInProgress, + onClick = state.onAddNewWalletClick, ) } } - AddWalletButton( - text = state.addNewWalletText, - isInProgress = state.isWalletSavingInProgress, - onClick = state.onAddNewWalletClick, - ) + } +} + +@Suppress("MagicNumber") +@Composable +private fun LazyItemScope.WalletItem( + model: UserWalletItemUM, + reorderableListState: ReorderableLazyListState, + walletReorderUM: WalletReorderUM, + modifier: Modifier = Modifier, +) { + ReorderableItem( + state = reorderableListState, + key = model.id, + modifier = modifier, + ) { isDragging -> + val elevation by animateDpAsState(if (isDragging) 9.dp else 0.dp) + val scale by animateFloatAsState(if (isDragging) 1.04f else 1f) + val shapeRadius by animateDpAsState(if (isDragging) 16.dp else 0.dp) + + Surface( + shape = RoundedCornerShape(shapeRadius), + shadowElevation = elevation, + modifier = Modifier.scale(scale), + ) { + UserWalletItem( + state = model, + modifier = Modifier + .longPressDraggableHandle( + enabled = walletReorderUM.isDragEnabled, + onDragStopped = walletReorderUM.onDragStopped, + ) + .background(color = TangemTheme.colors.background.primary), + ) + } } } @@ -109,6 +176,9 @@ private fun AddWalletButton( } } +private val WALLET_ITEM_HEIGHT = 72.dp +private val ADD_WALLET_BUTTON_HEIGHT = 60.dp + // region Preview @Composable @Preview(showBackground = true, widthDp = 360) 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 d1bd0b0529..256d99fb51 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 @@ -487,6 +487,7 @@ internal class WalletModel @Inject constructor( is WalletsUpdateActionResolver.Action.ReloadWallets -> { reloadWarnings(action) } + is WalletsUpdateActionResolver.Action.ReorderWallets -> reorderWallets(action) WalletsUpdateActionResolver.Action.EmptyWallets -> { Timber.w("Wallets list is empty!") } @@ -496,6 +497,25 @@ internal class WalletModel @Inject constructor( } } + private fun reorderWallets(action: WalletsUpdateActionResolver.Action.ReorderWallets) { + val currentWalletId = stateHolder.getSelectedWalletId() + val currentIndex = stateHolder.getWalletIndexByWalletId(userWalletId = currentWalletId) + + stateHolder.update( + transformer = ReorderWalletsTransformer( + wallets = action.wallets, + ), + ) + + val newIndex = stateHolder.getWalletIndexByWalletId(userWalletId = currentWalletId) + + if (currentIndex != null && newIndex != null && currentIndex != newIndex) { + scrollToWallet(prevIndex = currentIndex, newIndex = newIndex) { + stateHolder.update { it.copy(selectedWalletIndex = newIndex) } + } + } + } + private fun reloadWarnings(action: WalletsUpdateActionResolver.Action.ReloadWallets) { action.wallets.forEach { walletScreenContentLoader.load( 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 4946ef99e9..1f63082dad 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 @@ -82,6 +82,9 @@ internal class WalletsUpdateActionResolver @Inject constructor( isWalletsCountChanged(state, wallets) -> { getChangeWalletsListAction(state, wallets, selectedWallet) } + isWalletsOrderChanged(state, wallets) -> { + Action.ReorderWallets(wallets = wallets) + } isAnotherWalletSelected(state, selectedWallet) -> { Action.ReinitializeNewWallet( prevWalletId = state.getPrevSelectedWallet().id, @@ -121,6 +124,13 @@ internal class WalletsUpdateActionResolver @Inject constructor( return prevWalletsSize != walletsSize } + private fun isWalletsOrderChanged(state: WalletScreenState, wallets: List): Boolean { + val prevWalletIds = state.wallets.map { it.walletCardState.id } + val newWalletIds = wallets.map { it.walletId } + + return prevWalletIds != newWalletIds + } + private fun getChangeWalletsListAction( state: WalletScreenState, wallets: List, @@ -394,6 +404,15 @@ internal class WalletsUpdateActionResolver @Inject constructor( val wallets: List, ) : Action() + data class ReorderWallets( + val wallets: List, + ) : Action() { + + override fun toString(): String { + return "ReorderWallets(wallets = ${wallets.joinToString { it.walletId.toString() }})" + } + } + data object EmptyWallets : Action() data object Unknown : Action() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReorderWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReorderWalletsTransformer.kt new file mode 100644 index 0000000000..334c41892c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReorderWalletsTransformer.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState +import kotlinx.collections.immutable.toImmutableList + +/** + * Transformer that reorders wallets according to the new order + * + * @property wallets wallets in the new order + */ +internal class ReorderWalletsTransformer( + private val wallets: List, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + val walletIdToIndex = wallets.withIndex().associate { it.value.walletId to it.index } + + val reorderedWallets = prevState.wallets + .sortedBy { walletIdToIndex[it.walletCardState.id] ?: Int.MAX_VALUE } + .toImmutableList() + + return if (reorderedWallets != prevState.wallets) { + prevState.copy(wallets = reorderedWallets) + } else { + prevState + } + } +} \ No newline at end of file From 3e95ebf89a69088f99539325185d0246e4c603e3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Feb 2026 17:04:58 +0500 Subject: [PATCH 034/106] Updated on 2026-08-14 --- .../kotlin/com/tangem/screens/MainScreenPageObject.kt | 4 +++- .../com/tangem/tests/balance/TotalBalanceLongTapTest.kt | 4 ++-- .../kotlin/com/tangem/tests/balance/TotalBalanceUpdateTest.kt | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt index 8d412d40e7..0e2ea89335 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt @@ -1,5 +1,6 @@ package com.tangem.screens +import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider @@ -186,7 +187,8 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) } val totalBalanceText: KNode = child { - hasParent(withTestTag(MainScreenTestTags.WALLET_BALANCE)) + hasAnyAncestor(withTestTag(MainScreenTestTags.WALLET_BALANCE)) + addSemanticsMatcher(SemanticsMatcher.keyIsDefined(SemanticsProperties.Text)) } val notificationYesButton: KNode = child { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceLongTapTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceLongTapTest.kt index 7eea7bf682..bae6abd799 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceLongTapTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceLongTapTest.kt @@ -31,8 +31,8 @@ class TotalBalanceLongTapTest : BaseTestCase() { } } } - step("Assert 'Rename' button is not displayed") { - onMainScreen { totalBalanceMenuRenameWallet.assertIsNotDisplayed() } + step("Assert 'Rename' button is displayed") { + onMainScreen { totalBalanceMenuRenameWallet.assertIsDisplayed() } } step("Assert 'Delete' button is not displayed") { onMainScreen { totalBalanceMenuDeleteWallet.assertIsNotDisplayed() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceUpdateTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceUpdateTest.kt index 60771fc2ea..2003821062 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceUpdateTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceUpdateTest.kt @@ -90,7 +90,6 @@ class TotalBalanceUpdateTest : BaseTestCase() { } step("Click on 'Add' button") { onDialog { addButton.clickWithAssertion() } - onDialog { addButton.clickWithAssertion() } //TODO: [REDACTED_JIRA] } step("Assert 'Continue' is not displayed") { onDialog { addButton.assertIsNotDisplayed() } From 49cf3b9cef0579abcf8f79656dd86ac8dac45ead Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Feb 2026 15:48:14 +0300 Subject: [PATCH 035/106] Updated on 2026-08-14 --- .../screens/SwapChooseTokenPageObject.kt | 32 +++++ .../com/tangem/screens/SwapTokenPageObject.kt | 41 +++++- .../MainScreenActionButtonsTest.kt | 2 +- .../TokenDetailsScreenActionButtonsTest.kt | 2 +- .../com/tangem/tests/swap/SwapStoriesTest.kt | 8 +- .../tangem/tests/swap/SwapTokenScreenTest.kt | 135 +++++++++++++++++- .../core/ui/test/SwapTokenScreenTestTags.kt | 7 +- .../tangem/feature/swap/ui/TransactionCard.kt | 19 ++- 8 files changed, 230 insertions(+), 16 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt new file mode 100644 index 0000000000..e8e256017c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt @@ -0,0 +1,32 @@ +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.TokenElementsTestTags +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 SwapChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasText(getResourceString(R.string.common_choose_token)) + } + + val myTokensTitle: KNode = child { + hasText(getResourceString(R.string.exchange_tokens_available_tokens_header)) + } + + fun tokenWithTitle(tokenTitle: String): KNode = child { + hasTestTag(TokenElementsTestTags.TOKEN_TITLE) + hasAnyDescendant(withText(tokenTitle)) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onSwapChooseTokenScreen(function: SwapChooseTokenPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt index f31becbd3f..15be0dee77 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt @@ -9,6 +9,7 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose 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 class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { @@ -80,11 +81,45 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) hasText(getResourceString(R.string.common_swap)) } - fun tokenSymbol(symbol: String): KNode = child { - hasTestTag(SwapTokenScreenTestTags.TOKEN_SYMBOL) - hasText(symbol) + val youSwapBlock: KNode = child { + hasTestTag(SwapTokenScreenTestTags.SWAP_BLOCK_HEADER) + hasAnyDescendant(withText(getResourceString(R.string.swapping_from_title))) + hasAnyDescendant(withTestTag(SwapTokenScreenTestTags.BALANCE)) + useUnmergedTree = true } + val youReceiveBlock: KNode = child { + hasTestTag(SwapTokenScreenTestTags.SWAP_BLOCK_HEADER) + hasAnyDescendant(withText(getResourceString(R.string.swapping_to_title))) + hasAnyDescendant(withTestTag(SwapTokenScreenTestTags.BALANCE)) + useUnmergedTree = true + } + + val receiveFiatAmount: KNode = child { + hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT) + } + + val swapFiatAmount: KNode = child { + hasTestTag(SwapTokenScreenTestTags.SWAP_FIAT_AMOUNT) + } + + val changeTokenIcon: KNode = child { + hasTestTag(SwapTokenScreenTestTags.SELECT_TOKEN_ICON) + } + + fun swapTokenSymbol(symbol: String): KNode = child { + hasAnyAncestor(withTestTag(SwapTokenScreenTestTags.SWAP_CARD)) + hasTestTag(SwapTokenScreenTestTags.TOKEN_SYMBOL) + hasText(symbol) + useUnmergedTree = true + } + + fun receiveTokenSymbol(symbol: String): KNode = child { + hasAnyAncestor(withTestTag(SwapTokenScreenTestTags.RECEIVE_CARD)) + hasTestTag(SwapTokenScreenTestTags.TOKEN_SYMBOL) + hasText(symbol) + useUnmergedTree = true + } } internal fun BaseTestCase.onSwapTokenScreen(function: SwapTokenPageObject.() -> Unit) = 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 e807c84361..28fe6ef184 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt @@ -208,7 +208,7 @@ class MainScreenActionButtonsTest : BaseTestCase() { onSwapTokenScreen { title.assertIsDisplayed() } } step("Assert token symbol: '$tokenSymbol' is displayed") { - onSwapTokenScreen { tokenSymbol(tokenSymbol).assertIsDisplayed() } + onSwapTokenScreen { swapTokenSymbol(tokenSymbol).assertIsDisplayed() } } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt index 309a1e637c..5a158f6e07 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt @@ -119,7 +119,7 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() { onSwapTokenScreen { title.assertIsDisplayed() } } step("Assert token symbol: '$tokenSymbol' is displayed") { - onSwapTokenScreen { tokenSymbol(tokenSymbol).assertIsDisplayed() } + onSwapTokenScreen { swapTokenSymbol(tokenSymbol).assertIsDisplayed() } } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt index 2f3f1a81d6..21e65feb9b 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt @@ -135,7 +135,7 @@ class SwapStoriesTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses() } - step("Assert 'Swap' button has badge") { + step("Assert 'Swap' button has not badge") { onMainScreen { swapButton.assertHasBadge(false) } } step("Open 'Swap' screen") { @@ -191,7 +191,7 @@ class SwapStoriesTest : BaseTestCase() { step("Click on token with name: '$tokenName'") { onMainScreen { tokenWithTitleAndAddress(tokenName).performClick() } } - step("Assert 'Swap' button has badge") { + step("Assert 'Swap' button has not badge") { onTokenDetailsScreen { swapButton().assertHasBadge(false) } } step("Open 'Swap' screen") { @@ -216,7 +216,7 @@ class SwapStoriesTest : BaseTestCase() { } } - @AllureId("5471") + @AllureId("5470") @DisplayName("Check unavailable swap stories on 'Markets' token details screen") @Test fun checkUnavailableSwapStoriesOnMarketsTokenDetailsScreen() { @@ -245,7 +245,7 @@ class SwapStoriesTest : BaseTestCase() { step("Open 'Markets' token details screen for token '$tokenName'") { openMarketTokenDetailsScreen(blockchainName = tokenName, tokenName = tokenName) } - step("Assert 'Swap' button has badge") { + step("Assert 'Swap' button has not badge") { waitForIdle() onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertIsDisplayed() } onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeHidden) } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt index d0f574aa0b..60ec5a8154 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt @@ -10,7 +10,9 @@ import com.tangem.common.extensions.* import com.tangem.common.utils.resetWireMockScenarios import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.scenarios.SwapEntryPoint import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.openSwapScreen import com.tangem.scenarios.synchronizeAddresses import com.tangem.screens.* import dagger.hilt.android.testing.HiltAndroidTest @@ -218,7 +220,6 @@ class SwapTokenScreenTest : BaseTestCase() { onSwapSelectNetworkFeeBottomSheet { title.assertIsDisplayed() } } step("Assert 'Market' item is displayed") { - printSemanticTree(rootIndex = 1, useUnmergedTree = true) onSwapSelectNetworkFeeBottomSheet { marketSelectorItem.assertIsDisplayed() } } step("Assert 'Fast' item is displayed") { @@ -283,10 +284,138 @@ class SwapTokenScreenTest : BaseTestCase() { } } step("Assert token symbol: '$swapTokenSymbol' is displayed") { - onSwapTokenScreen { tokenSymbol(swapTokenSymbol).assertIsDisplayed() } + onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() } } step("Assert token symbol: '$receiveTokenSymbol' is displayed") { - onSwapTokenScreen { tokenSymbol(receiveTokenSymbol).assertIsDisplayed() } + onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() } + } + } + } + + @AllureId("575") + @DisplayName("Swap: check UI") + @Test + fun checkSwapUiTest() { + val swapTokenSymbol = "POL" + val receiveTokenSymbol = "ETH" + val newReceiveToken = "POL (ex-MATIC)" + val tokenTitle = "Polygon" + val inputAmount = "1" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'Close' button is displayed") { + onSwapTokenScreen { closeButton.assertIsDisplayed() } + } + step("Assert swap token symbol: '$swapTokenSymbol' is displayed") { + onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() } + } + step("Assert receive token symbol: '$receiveTokenSymbol' is displayed") { + onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() } + } + step("Click on 'Select token' icon") { + onSwapTokenScreen { changeTokenIcon.performClick() } + } + step("Select new receive token: $newReceiveToken") { + onSwapChooseTokenScreen { tokenWithTitle(newReceiveToken).performClick() } + } + step("Assert new receive token symbol: '$swapTokenSymbol' is displayed") { + onSwapTokenScreen { receiveTokenSymbol(swapTokenSymbol).assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert input amount = '$inputAmount'") { + onSwapTokenScreen { textInput.assertTextEquals(inputAmount) } + } + step("Press 'Delete' button on keyboard") { + device.uiDevice.pressDelete() + waitForIdle() + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Assert 'You receive' block is displayed") { + onSwapTokenScreen { youReceiveBlock } + } + step("Assert send token fiat amount is displayed") { + onSwapTokenScreen { swapFiatAmount.assertIsDisplayed() } + } + step("Assert receive token fiat amount is displayed") { + onSwapTokenScreen { receiveFiatAmount.assertIsDisplayed() } + } + step("Assert 'Swap tokens on screen' button is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + swapTokensOnscreenButton.assertIsDisplayed() + } + } + } + step("Assert 'Swap' button is disabled") { + onSwapTokenScreen { swapButton.assertIsNotEnabled() } + } + } + } + + @AllureId("5162") + @DisplayName("Swap: check swap tokens switch") + @Test + fun checkSwapTokensSwitchTest() { + val swapTokenSymbol = "POL" + val receiveTokenSymbol = "ETH" + val tokenTitle = "Polygon" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert swap token symbol: '$swapTokenSymbol' is displayed") { + onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() } + } + step("Assert receive token symbol: '$receiveTokenSymbol' is displayed") { + onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() } + } + step("Click on 'Swap tokens on screen' button") { + onSwapTokenScreen { swapTokensOnscreenButton.performClick() } + waitForIdle() + } + step("Assert new swap token symbol: '$receiveTokenSymbol' is displayed") { + onSwapTokenScreen { swapTokenSymbol(receiveTokenSymbol).assertIsDisplayed() } + } + step("Assert new receive token symbol: '$swapTokenSymbol' is displayed") { + onSwapTokenScreen { receiveTokenSymbol(swapTokenSymbol).assertIsDisplayed() } } } } 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 ec8883b577..3ddbefea69 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 @@ -2,14 +2,19 @@ package com.tangem.core.ui.test object SwapTokenScreenTestTags { const val CONTAINER = "SWAP_TOKEN_SCREEN_CONTAINER" - const val SWAP_BLOCK_HEADER = "SWAP_TOKEN_SCREEN_SWAP_BLOCK" + const val SWAP_BLOCK_HEADER = "SWAP_TOKEN_SCREEN_SWAP_BLOCK_HEADER" const val BALANCE = "SWAP_TOKEN_SCREEN_BALANCE" const val SWAP_TEXT_FIELD = "SWAP_TOKEN_SCREEN_SWAP_TEXT_FIELD" const val RECEIVE_TEXT_FIELD = "SWAP_TOKEN_SCREEN_RECEIVE_TEXT_FIELD" + const val SWAP_CARD = "SWAP_TOKEN_SCREEN_SWAP_CARD" + const val RECEIVE_CARD = "SWAP_TOKEN_SCREEN_RECEIVE_CARD" const val RECEIVE_AMOUNT_SHIMMER = "SWAP_TOKEN_SCREEN_RECEIVE_AMOUNT_SHIMMER" const val PROVIDERS_BLOCK = "SWAP_TOKEN_SCREEN_PROVIDERS_BLOCK" const val SWAP_BUTTON = "SWAP_TOKEN_SCREEN_SWAP_BUTTON" const val TOKEN = "SWAP_TOKEN_SCREEN_TOKEN" const val TOKEN_SYMBOL = "SWAP_TOKEN_SCREEN_TOKEN_SYMBOL" 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 SWAP_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_SWAP_FIAT_AMOUNT" } \ No newline at end of file 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 373d309f0f..25aac0f046 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 @@ -67,13 +67,21 @@ fun TransactionCard( @DrawableRes networkIconRes: Int? = null, onChangeTokenClick: (() -> Unit)? = null, ) { + val cardTag = when (type) { + is TransactionCardType.Inputtable -> + SwapTokenScreenTestTags.SWAP_CARD + is TransactionCardType.ReadOnly -> + SwapTokenScreenTestTags.RECEIVE_CARD + } + Box( modifier = modifier .background( shape = RoundedCornerShape(TangemTheme.dimens.radius16), color = TangemTheme.colors.background.primary, ) - .fillMaxSize(), + .fillMaxSize() + .testTag(cardTag), ) { Column( modifier = Modifier @@ -327,6 +335,7 @@ private fun Content( text = amount, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, + modifier = Modifier.testTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT), ) } } @@ -357,7 +366,9 @@ private fun Content( text = amount, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, - modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20), + modifier = Modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size20) + .testTag(SwapTokenScreenTestTags.SWAP_FIAT_AMOUNT), ) } } @@ -504,7 +515,9 @@ fun ChangeTokenSelector() { contentAlignment = Alignment.CenterEnd, ) { Icon( - modifier = Modifier.size(TangemTheme.dimens.size20), + modifier = Modifier + .size(TangemTheme.dimens.size20) + .testTag(SwapTokenScreenTestTags.SELECT_TOKEN_ICON), painter = painterResource(id = R.drawable.ic_chevron_24), tint = TangemTheme.colors.icon.secondary, contentDescription = null, From e17029ce9b2c2818a46148b9f4c1bc6f272290cf Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Feb 2026 13:31:07 +0400 Subject: [PATCH 036/106] Updated on 2026-08-14 --- .../DefaultCardCryptoCurrencyFactory.kt | 8 ++-- .../data/common/currency/UserTokensSaver.kt | 6 +-- .../tangem/data/common/di/DataCommonModule.kt | 14 +++--- .../wallet/DefaultWalletServerBinder.kt | 6 +-- .../DefaultCardCryptoCurrencyFactoryTest.kt | 20 ++++---- .../common/currency/UserTokensSaverTest.kt | 12 ++--- .../wallet/DefaultWalletServerBinderTest.kt | 26 +++++------ .../markets/DefaultMarketsTokenRepository.kt | 6 +-- .../data/markets/di/MarketsDataModule.kt | 6 +-- data/networks/build.gradle.kts | 7 +-- .../DefaultMultiNetworkStatusProducer.kt | 6 +-- .../DefaultMultiNetworkStatusProducerTest.kt | 34 +++++++------- data/onramp/detekt-baseline-debug.xml | 17 ------- .../data/onramp/DefaultHotCryptoRepository.kt | 29 ++++++------ .../data/onramp/DefaultOnrampRepository.kt | 46 ++++++++++--------- .../DefaultOnrampTransactionRepository.kt | 2 +- .../tangem/data/onramp/di/OnrampDataModule.kt | 6 +-- data/tokens/detekt-baseline-debug.xml | 10 ---- .../AccountListCryptoCurrenciesFetcher.kt | 9 ++-- ...faultMultiWalletCryptoCurrenciesFetcher.kt | 7 ++- ...ultiWalletCryptoCurrenciesFetcherModule.kt | 8 ++-- .../tangem/data/tokens/di/TokensDataModule.kt | 6 +-- .../repository/DefaultCurrenciesRepository.kt | 32 ++++++------- ...aultYieldSupplyWarningsViewedRepository.kt | 4 +- .../data/tokens/utils/CustomTokensMerger.kt | 4 +- .../AccountListCryptoCurrenciesFetcherTest.kt | 20 ++++---- ...tMultiWalletCryptoCurrenciesFetcherTest.kt | 40 ++++++++-------- 27 files changed, 184 insertions(+), 207 deletions(-) delete mode 100644 data/onramp/detekt-baseline-debug.xml delete mode 100644 data/tokens/detekt-baseline-debug.xml diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt index 2d7a8855d7..c10ff92538 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt @@ -6,9 +6,9 @@ import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.data.common.tokens.getDefaultWalletBlockchains import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency @@ -29,7 +29,7 @@ import com.tangem.domain.models.wallet.isMultiCurrency internal class DefaultCardCryptoCurrencyFactory( private val demoConfig: DemoConfig, private val excludedBlockchains: ExcludedBlockchains, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val accountsFeatureToggles: AccountsFeatureToggles, private val walletAccountsFetcher: WalletAccountsFetcher, private val userTokensResponseStore: UserTokensResponseStore, @@ -42,7 +42,7 @@ internal class DefaultCardCryptoCurrencyFactory( userWalletId: UserWalletId, networks: Set, ): Map> { - val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = userWalletId) // multi-currency wallet if (userWallet !is UserWallet.Cold || userWallet.isMultiCurrency) { @@ -67,7 +67,7 @@ internal class DefaultCardCryptoCurrencyFactory( } override suspend fun createByRawId(userWalletId: UserWalletId, network: Network.RawID): List { - val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = userWalletId) val blockchain = network.toBlockchain() 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 11d6ab08c9..f203b6bb3c 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 @@ -11,8 +11,8 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +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.coroutines.CoroutineDispatcherProvider @@ -24,7 +24,7 @@ import timber.log.Timber @Suppress("LongParameterList") class UserTokensSaver( private val tangemTechApi: TangemTechApi, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val userTokensResponseStore: UserTokensResponseStore, private val dispatchers: CoroutineDispatcherProvider, private val addressesEnricher: UserTokensResponseAddressesEnricher, @@ -59,7 +59,7 @@ class UserTokensSaver( useEnricher: Boolean = true, onFailSend: () -> Unit = {}, ) = withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) + val userWallet = userWalletsListRepository.getSyncOrNull(id = userWalletId) if (userWallet == null) { Timber.e("UserWallet with id $userWalletId not found. Cannot push tokens.") diff --git a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt index c81b908703..9ce3a6b121 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt @@ -13,8 +13,8 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.repository.WalletsRepository @@ -36,7 +36,7 @@ internal object DataCommonModule { @Singleton fun provideCardCryptoCurrencyFactory( excludedBlockchains: ExcludedBlockchains, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, accountsFeatureToggles: AccountsFeatureToggles, walletAccountsFetcher: WalletAccountsFetcher, userTokensResponseStore: UserTokensResponseStore, @@ -45,7 +45,7 @@ internal object DataCommonModule { return DefaultCardCryptoCurrencyFactory( demoConfig = DemoConfig, excludedBlockchains = excludedBlockchains, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, accountsFeatureToggles = accountsFeatureToggles, walletAccountsFetcher = walletAccountsFetcher, userTokensResponseStore = userTokensResponseStore, @@ -71,7 +71,7 @@ internal object DataCommonModule { @Singleton fun provideUserTokensSaver( tangemTechApi: TangemTechApi, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, userTokensResponseStore: UserTokensResponseStore, dispatchers: CoroutineDispatcherProvider, addressesEnricher: UserTokensResponseAddressesEnricher, @@ -81,7 +81,7 @@ internal object DataCommonModule { ): UserTokensSaver { return UserTokensSaver( tangemTechApi = tangemTechApi, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, userTokensResponseStore = userTokensResponseStore, dispatchers = dispatchers, addressesEnricher = addressesEnricher, @@ -109,13 +109,13 @@ internal object DataCommonModule { @Provides @Singleton fun provideWalletServerBinder( - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, appsFlyerStore: AppsFlyerStore, tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider, ): WalletServerBinder { return DefaultWalletServerBinder( - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, appsFlyerStore = appsFlyerStore, tangemTechApi = tangemTechApi, dispatchers = dispatchers, diff --git a/data/common/src/main/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinder.kt b/data/common/src/main/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinder.kt index f34c04f916..ee977ee128 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinder.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinder.kt @@ -4,21 +4,21 @@ import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter import com.tangem.datasource.local.appsflyer.AppsFlyerStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +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.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext internal class DefaultWalletServerBinder( - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val appsFlyerStore: AppsFlyerStore, private val tangemTechApi: TangemTechApi, private val dispatchers: CoroutineDispatcherProvider, ) : WalletServerBinder { override suspend fun bind(userWalletId: UserWalletId): ApiResponse? { - val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) ?: return null + val userWallet = userWalletsListRepository.getSyncOrNull(id = userWalletId) ?: return null return bind(userWallet) } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt index 1d2b947799..28aafd0069 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt @@ -12,10 +12,10 @@ import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.data.common.network.NetworkFactory import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.configs.GenericCardConfig +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -36,7 +36,7 @@ import org.junit.jupiter.params.ParameterizedTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DefaultCardCryptoCurrencyFactoryTest { - private val userWalletsStore: UserWalletsStore = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk() private val userTokensResponseStore: UserTokensResponseStore = mockk() private val excludedBlockchains = ExcludedBlockchains() private val accountsFeatureToggles = mockk() @@ -45,7 +45,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { private val factory = DefaultCardCryptoCurrencyFactory( demoConfig = DemoConfig, excludedBlockchains = excludedBlockchains, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, userTokensResponseStore = userTokensResponseStore, responseCryptoCurrenciesFactory = ResponseCryptoCurrenciesFactory( networkFactory = NetworkFactory(excludedBlockchains = excludedBlockchains), @@ -63,7 +63,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { @BeforeEach fun init() { - clearMocks(userWalletsStore, userTokensResponseStore, accountsFeatureToggles, walletAccountsFetcher, iconUri) + clearMocks(userWalletsListRepository, userTokensResponseStore, accountsFeatureToggles, walletAccountsFetcher, iconUri) mockkStatic(Uri::class) every { Uri.parse(any()) } returns iconUri @@ -82,7 +82,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { val network = ethereum.network every { accountsFeatureToggles.isFeatureEnabled } returns false - coEvery { userWalletsStore.getSyncStrict(key = userWallet.walletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncStrict(id = userWallet.walletId) } returns userWallet coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse // Act @@ -94,7 +94,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = userWallet.walletId) + userWalletsListRepository.getSyncStrict(id = userWallet.walletId) userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId) } } @@ -131,7 +131,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { // Arrange val userWallet = createSingleWallet() - coEvery { userWalletsStore.getSyncStrict(key = userWallet.walletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncStrict(id = userWallet.walletId) } returns userWallet // Act val actual = factory.create(userWalletId = userWallet.walletId, network = model.network) @@ -142,7 +142,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = userWallet.walletId) + userWalletsListRepository.getSyncStrict(id = userWallet.walletId) userWallet.scanResponse.cardTypesResolver.getBlockchain() } @@ -169,7 +169,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { // Arrange val userWallet = MockUserWalletFactory.createSingleWalletWithToken() - coEvery { userWalletsStore.getSyncStrict(key = userWallet.walletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncStrict(id = userWallet.walletId) } returns userWallet // Act val actual = factory.create(userWalletId = userWallet.walletId, network = model.network) @@ -186,7 +186,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = userWallet.walletId) + userWalletsListRepository.getSyncStrict(id = userWallet.walletId) userWallet.scanResponse.cardTypesResolver.getBlockchain() } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt index 6c79df0d81..9623d72208 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt @@ -8,8 +8,8 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +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.coroutines.TestingCoroutineDispatcherProvider @@ -23,7 +23,7 @@ import org.junit.jupiter.api.TestInstance class UserTokensSaverTest { private val tangemTechApi: TangemTechApi = mockk() - private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true) + private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxed = true) private val enricher: UserTokensResponseAddressesEnricher = mockk() private val accountsFeatureToggles = mockk { @@ -34,7 +34,7 @@ class UserTokensSaverTest { private val userTokensSaver: UserTokensSaver = UserTokensSaver( tangemTechApi = tangemTechApi, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, userTokensResponseStore = userTokensResponseStore, dispatchers = TestingCoroutineDispatcherProvider(), addressesEnricher = enricher, @@ -48,7 +48,7 @@ class UserTokensSaverTest { fun resetMocks() { clearMocks( tangemTechApi, - userWalletsStore, + userWalletsListRepository, userTokensResponseStore, enricher, walletServerBinder, @@ -119,7 +119,7 @@ class UserTokensSaverTest { var onFailSendCalled = false every { accountsFeatureToggles.isFeatureEnabled } returns true - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns userWallet coEvery { enricher(userWalletId, response) } returns enrichedResponse coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse @@ -165,7 +165,7 @@ class UserTokensSaverTest { walletType = WalletType.COLD, ) - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns userWallet coEvery { enricher(userWalletId, response) } returns enrichedResponse coEvery { tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse) diff --git a/data/common/src/test/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinderTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinderTest.kt index 6da352653b..e599ae0254 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinderTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinderTest.kt @@ -7,7 +7,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.WalletIdBody import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.datasource.local.appsflyer.AppsFlyerStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.AppsFlyerConversionData @@ -26,13 +26,13 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DefaultWalletServerBinderTest { - private val userWalletsStore: UserWalletsStore = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk() private val appsFlyerStore: AppsFlyerStore = mockk() private val tangemTechApi: TangemTechApi = mockk() private val dispatchers: CoroutineDispatcherProvider = TestingCoroutineDispatcherProvider() private val binder = DefaultWalletServerBinder( - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, appsFlyerStore = appsFlyerStore, tangemTechApi = tangemTechApi, dispatchers = dispatchers @@ -47,7 +47,7 @@ internal class DefaultWalletServerBinderTest { @AfterEach fun tearDown() { - clearMocks(userWalletsStore, appsFlyerStore, tangemTechApi) + clearMocks(userWalletsListRepository, appsFlyerStore, tangemTechApi) } @Nested @@ -65,7 +65,7 @@ internal class DefaultWalletServerBinderTest { ) val apiResponse = ApiResponse.Success(Unit) - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns userWallet coEvery { appsFlyerStore.get() } returns conversionData coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse @@ -74,22 +74,22 @@ internal class DefaultWalletServerBinderTest { Truth.assertThat(actual).isEqualTo(apiResponse) coVerifyOrder { - userWalletsStore.getSyncOrNull(userWalletId) + userWalletsListRepository.getSyncOrNull(userWalletId) appsFlyerStore.get() tangemTechApi.createWallet(requestBody) } } @Test - fun `bind will skipped if userWalletsStore returns null`() = runTest { - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns null + fun `bind will skipped if userWalletsListRepository returns null`() = runTest { + coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns null val actual = binder.bind(userWalletId = userWalletId) Truth.assertThat(actual).isEqualTo(null) coVerifyOrder { - userWalletsStore.getSyncOrNull(userWalletId) + userWalletsListRepository.getSyncOrNull(userWalletId) } coVerify(inverse = true) { @@ -107,7 +107,7 @@ internal class DefaultWalletServerBinderTest { ) val apiResponse = ApiResponse.Success(Unit) - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns userWallet coEvery { appsFlyerStore.get() } returns null coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse @@ -116,7 +116,7 @@ internal class DefaultWalletServerBinderTest { Truth.assertThat(actual).isEqualTo(apiResponse) coVerifyOrder { - userWalletsStore.getSyncOrNull(userWalletId) + userWalletsListRepository.getSyncOrNull(userWalletId) appsFlyerStore.get() tangemTechApi.createWallet(requestBody) } @@ -133,7 +133,7 @@ internal class DefaultWalletServerBinderTest { ) val apiResponse = ApiResponse.Error(ApiResponseError.TimeoutException()) as ApiResponse - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns userWallet coEvery { appsFlyerStore.get() } returns conversionData coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse @@ -142,7 +142,7 @@ internal class DefaultWalletServerBinderTest { Truth.assertThat(actual).isEqualTo(apiResponse) coVerifyOrder { - userWalletsStore.getSyncOrNull(userWalletId) + userWalletsListRepository.getSyncOrNull(userWalletId) appsFlyerStore.get() tangemTechApi.createWallet(requestBody) } diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 9c1b3be3f1..f3a58f68e2 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -19,8 +19,8 @@ import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.markets.models.response.TokenMarketExchangesResponse import com.tangem.datasource.local.datastore.RuntimeStateStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.models.account.DerivationIndex @@ -38,7 +38,7 @@ import java.util.concurrent.atomic.AtomicLong internal class DefaultMarketsTokenRepository( private val marketsApi: TangemTechMarketsApi, private val quotesFetcher: QuotesFetcher, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val dispatcherProvider: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val cacheRegistry: CacheRegistry, @@ -251,7 +251,7 @@ internal class DefaultMarketsTokenRepository( network: TokenMarketInfo.Network, accountIndex: DerivationIndex?, ): CryptoCurrency? { - val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("UserWalletId [$userWalletId] not found") + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) val blockchain = Blockchain.fromNetworkId(network.networkId) ?: error("Unknown network [${network.networkId}]") return if (network.contractAddress == null) { diff --git a/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt index 7632ec2cb1..1e216b4717 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt @@ -8,7 +8,7 @@ import com.tangem.data.common.quote.QuotesFetcher import com.tangem.data.markets.DefaultMarketsTokenRepository import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.local.datastore.RuntimeStateStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -26,7 +26,7 @@ internal object MarketsDataModule { fun provideMarketsTokenRepository( marketsApi: TangemTechMarketsApi, quotesFetcher: QuotesFetcher, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, dispatchers: CoroutineDispatcherProvider, analyticsEventHandler: AnalyticsEventHandler, cacheRegistry: CacheRegistry, @@ -37,7 +37,7 @@ internal object MarketsDataModule { marketsApi = marketsApi, quotesFetcher = quotesFetcher, dispatcherProvider = dispatchers, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, analyticsEventHandler = analyticsEventHandler, cacheRegistry = cacheRegistry, tokenExchangesStore = RuntimeStateStore(defaultValue = emptyList()), diff --git a/data/networks/build.gradle.kts b/data/networks/build.gradle.kts index c61859dc69..90ca5576e0 100644 --- a/data/networks/build.gradle.kts +++ b/data/networks/build.gradle.kts @@ -24,11 +24,12 @@ dependencies { // endregion // region Project - Domain - implementation(projects.domain.legacy) - implementation(projects.domain.walletManager) implementation(projects.domain.card) - api(projects.domain.models) + implementation(projects.domain.common) + implementation(projects.domain.legacy) + implementation(projects.domain.models) implementation(projects.domain.networks) + implementation(projects.domain.walletManager) // endregion // region Project - Libs 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 895dc129a7..f0546b2d68 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 @@ -4,7 +4,7 @@ import arrow.core.Option import arrow.core.some import com.tangem.data.common.network.NetworkFactory import com.tangem.data.networks.store.NetworksStatusesStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.networks.multi.MultiNetworkStatusProducer @@ -28,7 +28,7 @@ internal class DefaultMultiNetworkStatusProducer @AssistedInject constructor( @Assisted val params: MultiNetworkStatusProducer.Params, override val flowProducerTools: FlowProducerTools, private val networksStatusesStore: NetworksStatusesStore, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val networkFactory: NetworkFactory, private val dispatchers: CoroutineDispatcherProvider, ) : MultiNetworkStatusProducer { @@ -39,7 +39,7 @@ internal class DefaultMultiNetworkStatusProducer @AssistedInject constructor( return networksStatusesStore.get(userWalletId = params.userWalletId) .distinctUntilChanged() .mapNotNull { statuses -> - val userWallet = userWalletsStore.getSyncOrNull(params.userWalletId) + val userWallet = userWalletsListRepository.getSyncOrNull(params.userWalletId) if (userWallet == null) { Timber.e("Unable to get UserWallet with provided ID: ${params.userWalletId}") diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt index aa5c37e306..f677f10935 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt @@ -9,8 +9,8 @@ import com.tangem.data.common.network.NetworkFactory import com.tangem.data.networks.models.SimpleNetworkStatus import com.tangem.data.networks.store.NetworksStatusesStore import com.tangem.data.networks.toSimple -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.configs.GenericCardConfig +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.networks.multi.MultiNetworkStatusProducer @@ -33,7 +33,7 @@ internal class DefaultMultiNetworkStatusProducerTest { private val params = MultiNetworkStatusProducer.Params(userWalletId = userWallet.walletId) private val networksStatusesStore = mockk() - private val userWalletsStore = mockk() + private val userWalletsListRepository = mockk() private val networkFactory = mockk() private val dispatchers = TestingCoroutineDispatcherProvider() private val flowProducerTools: FlowProducerTools = mockk() @@ -41,7 +41,7 @@ internal class DefaultMultiNetworkStatusProducerTest { private val producer = DefaultMultiNetworkStatusProducer( params = params, networksStatusesStore = networksStatusesStore, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, networkFactory = networkFactory, dispatchers = dispatchers, flowProducerTools = flowProducerTools, @@ -49,7 +49,7 @@ internal class DefaultMultiNetworkStatusProducerTest { @BeforeEach fun resetMocks() { - clearMocks(networksStatusesStore, userWalletsStore, networkFactory) + clearMocks(networksStatusesStore, userWalletsListRepository, networkFactory) } @Test @@ -65,7 +65,7 @@ internal class DefaultMultiNetworkStatusProducerTest { val networksStatusesFlow = flowOf(simpleStatuses) every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow - every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + every { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet every { networkFactory.create( @@ -94,7 +94,7 @@ internal class DefaultMultiNetworkStatusProducerTest { verifyOrder { networksStatusesStore.get(params.userWalletId) - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.getSyncOrNull(params.userWalletId) networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, @@ -129,7 +129,7 @@ internal class DefaultMultiNetworkStatusProducerTest { // region every every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow - every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + every { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet every { networkFactory.create( networkId = simpleStatuses.first().id, @@ -177,7 +177,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.getSyncOrNull(params.userWalletId) networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, @@ -202,7 +202,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual2).isEqualTo(expected2) verifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.getSyncOrNull(params.userWalletId) networkFactory.create( networkId = updatedSimpleStatuses.first().id, derivationPath = updatedSimpleStatuses.first().id.derivationPath, @@ -230,7 +230,7 @@ internal class DefaultMultiNetworkStatusProducerTest { // region every every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow - every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + every { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet every { networkFactory.create( @@ -263,7 +263,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.getSyncOrNull(params.userWalletId) networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, @@ -312,7 +312,7 @@ internal class DefaultMultiNetworkStatusProducerTest { // region every every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow - every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + every { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet every { networkFactory.create( networkId = simpleStatuses.first().id, @@ -340,7 +340,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder(inverse = true) { - userWalletsStore.getSyncOrNull(any()) + userWalletsListRepository.getSyncOrNull(any()) networkFactory.create(networkId = any(), derivationPath = any(), userWallet = any()) } @@ -354,7 +354,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual2.first()).isEqualTo(expected2) verifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.getSyncOrNull(params.userWalletId) networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, @@ -382,7 +382,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual.first()).isEqualTo(expected) verify { networksStatusesStore.get(params.userWalletId) } - verify(inverse = true) { userWalletsStore.getSyncOrNull(params.userWalletId) } + verify(inverse = true) { userWalletsListRepository.getSyncOrNull(params.userWalletId) } } @Test @@ -398,7 +398,7 @@ internal class DefaultMultiNetworkStatusProducerTest { val networksStatusesFlow = flowOf(simpleStatuses) every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow - every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + every { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { networkFactory.create(networkId = any(), any(), any()) } returns null // Act @@ -411,7 +411,7 @@ internal class DefaultMultiNetworkStatusProducerTest { verifyOrder { networksStatusesStore.get(params.userWalletId) - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.getSyncOrNull(params.userWalletId) networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, diff --git a/data/onramp/detekt-baseline-debug.xml b/data/onramp/detekt-baseline-debug.xml deleted file mode 100644 index ec3fa17add..0000000000 --- a/data/onramp/detekt-baseline-debug.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - BooleanPropertyNaming:DefaultOnrampRepository.kt$DefaultOnrampRepository$val cachedValue = onrampSepaAvailabilityStore.getSyncOrNull(key) - MultilineLambdaItParameter:DefaultHotCryptoRepository.kt$DefaultHotCryptoRepository${ Timber.e(it, "Unable to fetch hot crypto") analyticsEventHandler.send( event = MainScreenAnalyticsEvent.HotTokenError( errorCode = (it as? ApiResponseError.HttpException)?.code?.numericCode?.toString().orEmpty(), ), ) } - MultilineLambdaItParameter:DefaultHotCryptoRepository.kt$DefaultHotCryptoRepository${ if (it.id == OLD_POLYGON_NAME) { it.copy(id = NEW_POLYGON_NAME) } else { it } } - MultilineLambdaItParameter:DefaultHotCryptoRepository.kt$DefaultHotCryptoRepository${ val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("UserWalletId [$userWalletId] not found") HotCryptoCurrencyConverter( userWallet = userWallet, imageHost = it.imageHost, excludedBlockchains = excludedBlockchains, ) .convertList(input = it.tokens) .filterNotNull() } - MultilineLambdaItParameter:DefaultOnrampRepository.kt$DefaultOnrampRepository${ Timber.w(it, "Unable to fetch express providers") throw it } - MultilineLambdaItParameter:DefaultOnrampRepository.kt$DefaultOnrampRepository${ Timber.w(it, "Unable to fetch onramp pairs") throw it } - MultilineLambdaItParameter:DefaultOnrampRepository.kt$DefaultOnrampRepository${ Timber.w(it, "Unable to fetch onramp payment methods") throw it } - UnnecessaryLet:DefaultOnrampRepository.kt$DefaultOnrampRepository$let(countryConverter::convert) - UnnecessaryLet:DefaultOnrampRepository.kt$DefaultOnrampRepository$let(paymentMethodsConverter::convertList) - UnnecessaryLet:DefaultOnrampRepository.kt$DefaultOnrampRepository$let(statusConverter::convert) - UseOrEmpty:DefaultOnrampTransactionRepository.kt$DefaultOnrampTransactionRepository$stored ?: emptySet() - - 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 5b985a43fc..aa5a415297 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 @@ -18,10 +18,10 @@ import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.extensions.canHandleBlockchain import com.tangem.domain.card.common.extensions.canHandleToken +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.HotCryptoCurrency @@ -54,7 +54,7 @@ import timber.log.Timber internal class DefaultHotCryptoRepository( private val excludedBlockchains: ExcludedBlockchains, private val hotCryptoResponseStore: HotCryptoResponseStore, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val tangemTechApi: TangemTechApi, private val appCurrencyResponseStore: AppCurrencyResponseStore, private val accountsFeatureToggles: AccountsFeatureToggles, @@ -88,22 +88,22 @@ internal class DefaultHotCryptoRepository( return hotCryptoResponseStore.get() .map { it[userWalletId] } .filterNotNull() - .map { - val userWallet = userWalletsStore.getSyncOrNull(userWalletId) + .map { response -> + val userWallet = userWalletsListRepository.getSyncOrNull(userWalletId) ?: error("UserWalletId [$userWalletId] not found") HotCryptoCurrencyConverter( userWallet = userWallet, - imageHost = it.imageHost, + imageHost = response.imageHost, excludedBlockchains = excludedBlockchains, ) - .convertList(input = it.tokens) + .convertList(input = response.tokens) .filterNotNull() } } private fun getWalletsWithTokensFlow(): Flow>> { - return userWalletsStore.userWallets.flatMapLatest { userWallets -> + return userWalletsListRepository.loadAndGet().flatMapLatest { userWallets -> val flows = userWallets.map { userWallet -> if (accountsFeatureToggles.isFeatureEnabled) { walletAccountsFetcher.get(userWalletId = userWallet.walletId).map { it.toUserTokensResponse() } @@ -131,12 +131,13 @@ internal class DefaultHotCryptoRepository( tangemTechApi.getHotCrypto(currencyId = appCurrencyId).getOrThrow() } .onSuccess { Timber.d("HotCrypto is successfully updated") } - .onFailure { - Timber.e(it, "Unable to fetch hot crypto") + .onFailure { throwable -> + Timber.e(throwable, "Unable to fetch hot crypto") + val httpException = throwable as? ApiResponseError.HttpException analyticsEventHandler.send( event = MainScreenAnalyticsEvent.HotTokenError( - errorCode = (it as? ApiResponseError.HttpException)?.code?.numericCode?.toString().orEmpty(), + errorCode = httpException?.code?.numericCode?.toString().orEmpty(), ), ) } @@ -164,11 +165,11 @@ internal class DefaultHotCryptoRepository( } private fun List.applyTokensIdMigrations(): List { - return this.map { - if (it.id == OLD_POLYGON_NAME) { - it.copy(id = NEW_POLYGON_NAME) + return this.map { token -> + if (token.id == OLD_POLYGON_NAME) { + token.copy(id = NEW_POLYGON_NAME) } else { - it + token } } } 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 be4b3d4460..bf07c9a118 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 @@ -138,7 +138,7 @@ internal class DefaultOnrampRepository( } } - val country = onrampApi.getCountryByIp( + val response = onrampApi.getCountryByIp( userWalletId = userWallet.walletId.stringValue, refCode = ExpressUtils.getRefCode( userWallet = userWallet, @@ -146,7 +146,8 @@ internal class DefaultOnrampRepository( ), ) .getOrThrow() - .let(countryConverter::convert) + + val country = countryConverter.convert(response) onrampCurrentCountryByIPStore.store(country) @@ -154,7 +155,7 @@ internal class DefaultOnrampRepository( } override suspend fun getStatus(userWallet: UserWallet, txId: String): OnrampStatus = withContext(dispatchers.io) { - onrampApi.getStatus( + val response = onrampApi.getStatus( userWalletId = userWallet.walletId.stringValue, refCode = ExpressUtils.getRefCode( userWallet = userWallet, @@ -163,7 +164,8 @@ internal class DefaultOnrampRepository( txId = txId, ) .getOrThrow() - .let(statusConverter::convert) + + statusConverter.convert(response) } override suspend fun saveDefaultCurrency(currency: OnrampCurrency) = withContext(dispatchers.io) { @@ -218,9 +220,9 @@ internal class DefaultOnrampRepository( ), ).bind() }, - onError = { - Timber.w(it, "Unable to fetch onramp payment methods") - throw it + onError = { error -> + Timber.w(error, "Unable to fetch onramp payment methods") + throw error }, ) paymentMethodsStore.store(PAYMENT_METHODS_KEY, response.removeApplePay()) @@ -253,9 +255,9 @@ internal class DefaultOnrampRepository( ), ).bind() }, - onError = { - Timber.w(it, "Unable to fetch onramp pairs") - throw it + onError = { error -> + Timber.w(error, "Unable to fetch onramp pairs") + throw error }, ) } @@ -270,9 +272,9 @@ internal class DefaultOnrampRepository( ), ).bind() }, - onError = { - Timber.w(it, "Unable to fetch express providers") - throw it + onError = { error -> + Timber.w(error, "Unable to fetch express providers") + throw error }, ) } @@ -291,10 +293,10 @@ internal class DefaultOnrampRepository( cryptoCurrency = cryptoCurrency, ) - val cachedValue = onrampSepaAvailabilityStore.getSyncOrNull(key) + val isCachedValue = onrampSepaAvailabilityStore.getSyncOrNull(key) - if (cachedValue != null) { - return@withContext cachedValue + if (isCachedValue != null) { + return@withContext isCachedValue } val onrampPairs = @@ -318,9 +320,9 @@ internal class DefaultOnrampRepository( ), ).bind() }, - onError = { - Timber.w(it, "Unable to fetch onramp pairs") - throw it + onError = { error -> + Timber.w(error, "Unable to fetch onramp pairs") + throw error }, ) @@ -525,9 +527,11 @@ internal class DefaultOnrampRepository( } private suspend fun getPaymentMethods(): List { - return requireNotNull(paymentMethodsStore.getSyncOrNull(PAYMENT_METHODS_KEY)) { + val methods = requireNotNull(paymentMethodsStore.getSyncOrNull(PAYMENT_METHODS_KEY)) { "Onramp payment methods is absent in storage" - }.let(paymentMethodsConverter::convertList) + } + + return paymentMethodsConverter.convertList(methods) } private fun createOnrampTransaction( diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt index 782b4b7b5b..77c3740a75 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt @@ -100,7 +100,7 @@ internal class DefaultOnrampTransactionRepository( mutablePreferences.setObjectSet( key = PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY, - value = stored ?: emptySet(), + value = stored.orEmpty(), ) } } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index a4e93fec4f..88a6e8f021 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -28,8 +28,8 @@ import com.tangem.datasource.local.onramp.sepa.OnrampCurrentCountryByIPStore import com.tangem.datasource.local.onramp.sepa.OnrampSepaAvailabilityStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.onramp.repositories.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -105,7 +105,7 @@ internal object OnrampDataModule { fun provideHotCryptoRepository( excludedBlockchains: ExcludedBlockchains, hotCryptoResponseStore: HotCryptoResponseStore, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, tangemTechApi: TangemTechApi, appCurrencyResponseStore: AppCurrencyResponseStore, dispatchers: CoroutineDispatcherProvider, @@ -117,7 +117,7 @@ internal object OnrampDataModule { return DefaultHotCryptoRepository( excludedBlockchains = excludedBlockchains, hotCryptoResponseStore = hotCryptoResponseStore, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, tangemTechApi = tangemTechApi, appCurrencyResponseStore = appCurrencyResponseStore, dispatchers = dispatchers, diff --git a/data/tokens/detekt-baseline-debug.xml b/data/tokens/detekt-baseline-debug.xml deleted file mode 100644 index bc443d0652..0000000000 --- a/data/tokens/detekt-baseline-debug.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - MultilineLambdaItParameter:CustomTokensMerger.kt$CustomTokensMerger${ Timber.e(it, "Unable to fetch token:\n$token") null } - NullableToStringCall:AccountListCryptoCurrenciesFetcher.kt$AccountListCryptoCurrenciesFetcher$${this::class.simpleName} - NullableToStringCall:DefaultMultiWalletCryptoCurrenciesFetcher.kt$DefaultMultiWalletCryptoCurrenciesFetcher$${this::class.simpleName} - UseOrEmpty:DefaultYieldSupplyWarningsViewedRepository.kt$DefaultYieldSupplyWarningsViewedRepository$appPreferencesStore.getObjectSet<String>(PreferencesKeys.YIELD_SUPPLY_WARNINGS_STATES_KEY).firstOrNull() ?: emptySet() - - diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcher.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcher.kt index bf34ca207d..2eb230d05d 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcher.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcher.kt @@ -4,11 +4,10 @@ import arrow.core.Either import arrow.core.right import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.datasource.api.tangemTech.models.account.flattenTokens -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.utils.catchOn import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.express.models.ExpressAsset -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher.Params @@ -17,7 +16,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider /** * Implementation of [MultiWalletCryptoCurrenciesFetcher] that fetches crypto currencies of all accounts * - * @property userWalletsStore [UserWallet]'s store + * @property userWalletsListRepository repository to get user wallets * @property walletAccountsFetcher instance of [WalletAccountsFetcher] to fetch accounts for a multi wallet * @property expressServiceFetcher fetcher of express service * @property dispatchers dispatchers @@ -25,7 +24,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider [REDACTED_AUTHOR] */ internal class AccountListCryptoCurrenciesFetcher( - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val walletAccountsFetcher: WalletAccountsFetcher, private val expressServiceFetcher: ExpressServiceFetcher, private val dispatchers: CoroutineDispatcherProvider, @@ -33,7 +32,7 @@ internal class AccountListCryptoCurrenciesFetcher( override suspend fun invoke(params: Params): Either { return Either.catchOn(dispatchers.default) { - val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet") diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt index 8fc010aa4d..8e1b04b9bb 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt @@ -10,7 +10,7 @@ import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.utils.catchOn import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.express.ExpressServiceFetcher @@ -26,7 +26,6 @@ import timber.log.Timber /** * Default implementation of [MultiWalletCryptoCurrenciesFetcher] * - * @property userWalletsStore [UserWallet]'s store * @property tangemTechApi Tangem Tech API * @property userTokensResponseStore store of [UserTokensResponse] * @property userTokensSaver user tokens saver @@ -39,7 +38,7 @@ import timber.log.Timber @Suppress("LongParameterList") internal class DefaultMultiWalletCryptoCurrenciesFetcher( private val demoConfig: DemoConfig, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val tangemTechApi: TangemTechApi, private val customTokensMerger: CustomTokensMerger, private val userTokensResponseStore: UserTokensResponseStore, @@ -52,7 +51,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcher( private val userTokensResponseFactory = UserTokensResponseFactory() override suspend fun invoke(params: Params) = Either.catchOn(dispatchers.default) { - val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet") diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt index 8d06bf0af1..e3e6f70446 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt @@ -8,8 +8,8 @@ import com.tangem.data.tokens.DefaultMultiWalletCryptoCurrenciesFetcher import com.tangem.data.tokens.utils.CustomTokensMerger import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher @@ -29,7 +29,7 @@ internal class MultiWalletCryptoCurrenciesFetcherModule { fun provideMultiWalletCryptoCurrenciesFetcher( accountsFeatureToggles: AccountsFeatureToggles, tangemTechApi: TangemTechApi, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, userTokensResponseStore: UserTokensResponseStore, userTokensSaver: UserTokensSaver, cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, @@ -39,7 +39,7 @@ internal class MultiWalletCryptoCurrenciesFetcherModule { ): MultiWalletCryptoCurrenciesFetcher { return if (accountsFeatureToggles.isFeatureEnabled) { AccountListCryptoCurrenciesFetcher( - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, walletAccountsFetcher = walletAccountsFetcher, expressServiceFetcher = expressServiceFetcher, dispatchers = dispatchers, @@ -47,7 +47,7 @@ internal class MultiWalletCryptoCurrenciesFetcherModule { } else { DefaultMultiWalletCryptoCurrenciesFetcher( demoConfig = DemoConfig, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, tangemTechApi = tangemTechApi, customTokensMerger = CustomTokensMerger( tangemTechApi = tangemTechApi, 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 8c0efab318..f30629e0d4 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,8 +13,8 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.TokenReceiveWarningActionStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -39,7 +39,7 @@ internal object TokensDataModule { fun provideCurrenciesRepository( tangemTechApi: TangemTechApi, userTokensResponseStore: UserTokensResponseStore, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, walletManagersFacade: WalletManagersFacade, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, @@ -53,7 +53,7 @@ internal object TokensDataModule { ): CurrenciesRepository { return DefaultCurrenciesRepository( tangemTechApi = tangemTechApi, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, walletManagersFacade = walletManagersFacade, cacheRegistry = cacheRegistry, userTokensResponseStore = userTokensResponseStore, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index ffb98afb6c..c8755050b6 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -13,10 +13,10 @@ import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.error.DataError import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.express.ExpressServiceFetcher @@ -41,7 +41,7 @@ import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") internal class DefaultCurrenciesRepository( private val tangemTechApi: TangemTechApi, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val walletManagersFacade: WalletManagersFacade, private val cacheRegistry: CacheRegistry, private val expressServiceFetcher: ExpressServiceFetcher, @@ -104,7 +104,7 @@ internal class DefaultCurrenciesRepository( ) fetchExpressAssetsByNetworkIds( - userWallet = userWalletsStore.getSyncStrict(key = userWalletId), + userWallet = userWalletsListRepository.getSyncStrict(id = userWalletId), userTokens = updatedResponse, ) @@ -194,7 +194,7 @@ internal class DefaultCurrenciesRepository( override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { return channelFlow { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) if (userWallet.isMultiCurrency) { getMultiCurrencyWalletCurrenciesUpdates(userWalletId).collect(::send) @@ -211,7 +211,7 @@ internal class DefaultCurrenciesRepository( refresh: Boolean, ): CryptoCurrency { return withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) userWallet.requireColdWallet() ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) @@ -234,7 +234,7 @@ internal class DefaultCurrenciesRepository( refresh: Boolean, ): List { return withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) val scanResponse = userWallet.requireColdWallet().scanResponse val currencies = if (scanResponse.cardTypesResolver.isSingleWalletWithToken()) { @@ -261,7 +261,7 @@ internal class DefaultCurrenciesRepository( id: CryptoCurrency.ID, ): CryptoCurrency { return withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) userWallet.requireColdWallet() ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) @@ -277,7 +277,7 @@ internal class DefaultCurrenciesRepository( private fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { return channelFlow { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) getMultiCurrencyWalletCurrencies(userWallet) @@ -290,7 +290,7 @@ internal class DefaultCurrenciesRepository( userWalletId: UserWalletId, refresh: Boolean, ): List = withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) fetchTokensIfCacheExpired(userWallet, refresh) @@ -344,7 +344,7 @@ internal class DefaultCurrenciesRepository( derivationPath: Network.DerivationPath, ): CryptoCurrency.Coin { return withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true) fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false) @@ -378,7 +378,7 @@ internal class DefaultCurrenciesRepository( override fun isTokensGrouped(userWalletId: UserWalletId): Flow { return channelFlow { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) if (userWallet.isMultiCurrency) { getSavedUserTokensResponse(userWalletId) @@ -394,7 +394,7 @@ internal class DefaultCurrenciesRepository( override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { return channelFlow { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) if (userWallet.isMultiCurrency) { getSavedUserTokensResponse(userWalletId) @@ -481,7 +481,7 @@ internal class DefaultCurrenciesRepository( contractAddress: String, networkId: String, ): CryptoCurrency.Token { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) val token = withContext(dispatchers.io) { val foundToken = tangemTechApi.getCoins( contractAddress = contractAddress, @@ -513,7 +513,7 @@ internal class DefaultCurrenciesRepository( override fun getAllWalletsCryptoCurrencies( currencyRawId: CryptoCurrency.RawID, ): Flow>> { - return userWalletsStore.userWallets.flatMapLatest { userWallets -> + return userWalletsListRepository.loadAndGet().flatMapLatest { userWallets -> userWallets.filter { it.isMultiCurrency } .forEach { fetchTokensIfCacheExpired(userWallet = it, refresh = false) } @@ -589,7 +589,7 @@ internal class DefaultCurrenciesRepository( } override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver? { - return (userWalletsStore.getSyncStrict(userWalletId) as? UserWallet.Cold)?.cardTypesResolver + return (userWalletsListRepository.getSyncStrict(userWalletId) as? UserWallet.Cold)?.cardTypesResolver } private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { @@ -711,7 +711,7 @@ internal class DefaultCurrenciesRepository( ) private fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultYieldSupplyWarningsViewedRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultYieldSupplyWarningsViewedRepository.kt index 193bbb03c6..c9662d98f7 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultYieldSupplyWarningsViewedRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultYieldSupplyWarningsViewedRepository.kt @@ -14,8 +14,8 @@ internal class DefaultYieldSupplyWarningsViewedRepository( ) : YieldSupplyWarningsViewedRepository { override suspend fun getViewedWarnings(): Set = withContext(dispatchers.io) { - appPreferencesStore.getObjectSet(PreferencesKeys.YIELD_SUPPLY_WARNINGS_STATES_KEY).firstOrNull() - ?: emptySet() + appPreferencesStore.getObjectSet(PreferencesKeys.YIELD_SUPPLY_WARNINGS_STATES_KEY) + .firstOrNull().orEmpty() } override suspend fun view(symbol: String) = withContext(dispatchers.io) { 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 2467b477da..4d0cf9c1fc 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 @@ -104,8 +104,8 @@ internal class CustomTokensMerger( networkIds = token.networkId, ).bind() }, - onError = { - Timber.e(it, "Unable to fetch token:\n$token") + onError = { error -> + Timber.e(error, "Unable to fetch token:\n$token") null }, ) diff --git a/data/tokens/src/test/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcherTest.kt b/data/tokens/src/test/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcherTest.kt index 7535e139d9..3e544d681b 100644 --- a/data/tokens/src/test/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcherTest.kt +++ b/data/tokens/src/test/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcherTest.kt @@ -4,7 +4,7 @@ import arrow.core.left import arrow.core.right import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -22,13 +22,13 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class AccountListCryptoCurrenciesFetcherTest { - private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true) + private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) private val walletAccountsFetcher: WalletAccountsFetcher = mockk(relaxUnitFun = true) private val expressServiceFetcher: ExpressServiceFetcher = mockk() private val dispatchers = TestingCoroutineDispatcherProvider() private val fetcher = AccountListCryptoCurrenciesFetcher( - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, walletAccountsFetcher = walletAccountsFetcher, expressServiceFetcher = expressServiceFetcher, dispatchers = dispatchers, @@ -36,7 +36,7 @@ internal class AccountListCryptoCurrenciesFetcherTest { @BeforeEach fun resetMocks() { - clearMocks(userWalletsStore, walletAccountsFetcher) + clearMocks(userWalletsListRepository, walletAccountsFetcher) } @Test @@ -44,7 +44,7 @@ internal class AccountListCryptoCurrenciesFetcherTest { // Arrange val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) val mockUserWallet = mockk { every { isMultiCurrency } returns false } - every { userWalletsStore.getSyncStrict(key = params.userWalletId) } returns mockUserWallet + every { userWalletsListRepository.getSyncStrict(id = params.userWalletId) } returns mockUserWallet // Act val actual = fetcher(params) @@ -55,7 +55,7 @@ internal class AccountListCryptoCurrenciesFetcherTest { ).left() assertEither(actual, expected) - verify { userWalletsStore.getSyncStrict(key = params.userWalletId) } + verify { userWalletsListRepository.getSyncStrict(id = params.userWalletId) } coVerify(inverse = true) { walletAccountsFetcher.fetch(any()) } } @@ -66,7 +66,7 @@ internal class AccountListCryptoCurrenciesFetcherTest { val mockUserWallet = mockk { every { isMultiCurrency } returns true } val response = mockk(relaxed = true) - every { userWalletsStore.getSyncStrict(key = params.userWalletId) } returns mockUserWallet + every { userWalletsListRepository.getSyncStrict(id = params.userWalletId) } returns mockUserWallet coEvery { walletAccountsFetcher.fetch(userWalletId = params.userWalletId) } returns response coEvery { expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = emptySet()) } returns Unit.right() @@ -77,7 +77,7 @@ internal class AccountListCryptoCurrenciesFetcherTest { assertEitherRight(actual) coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.getSyncStrict(id = params.userWalletId) walletAccountsFetcher.fetch(userWalletId = params.userWalletId) expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = emptySet()) } @@ -90,7 +90,7 @@ internal class AccountListCryptoCurrenciesFetcherTest { val mockUserWallet = mockk { every { isMultiCurrency } returns true } val error = RuntimeException("fetch error") - every { userWalletsStore.getSyncStrict(key = params.userWalletId) } returns mockUserWallet + every { userWalletsListRepository.getSyncStrict(id = params.userWalletId) } returns mockUserWallet coEvery { walletAccountsFetcher.fetch(userWalletId = params.userWalletId) } throws error // Act @@ -101,7 +101,7 @@ internal class AccountListCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.getSyncStrict(id = params.userWalletId) walletAccountsFetcher.fetch(userWalletId = params.userWalletId) } } diff --git a/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt index 991d4f40f5..b0a5170a5a 100644 --- a/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt +++ b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt @@ -13,7 +13,7 @@ import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.express.models.ExpressAsset @@ -38,7 +38,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() private val userTokensResponseFactory = UserTokensResponseFactory() - private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true) + private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) private val tangemTechApi: TangemTechApi = mockk() private val customTokensMerger: CustomTokensMerger = mockk() private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true) @@ -48,7 +48,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { private val fetcher = DefaultMultiWalletCryptoCurrenciesFetcher( demoConfig = DemoConfig, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, tangemTechApi = tangemTechApi, customTokensMerger = customTokensMerger, userTokensResponseStore = userTokensResponseStore, @@ -61,7 +61,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { @BeforeEach fun resetMocks() { clearMocks( - userWalletsStore, + userWalletsListRepository, tangemTechApi, userTokensResponseStore, userTokensSaver, @@ -79,7 +79,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { every { isMultiCurrency } returns false } - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet // Act val actual = fetcher(params) @@ -90,7 +90,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { ).left() assertEither(actual, expected) - verifyOrder { userWalletsStore.getSyncStrict(key = params.userWalletId) } + verifyOrder { userWalletsListRepository.getSyncStrict(id = params.userWalletId) } coVerify(inverse = true) { userTokensResponseStore.getSyncOrNull(any()) } @@ -121,7 +121,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { ), ) - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns null every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) @@ -145,7 +145,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.getSyncStrict(id = params.userWalletId) userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) @@ -169,7 +169,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN), ) - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns defaultResponse coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { @@ -191,7 +191,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.getSyncStrict(id = params.userWalletId) userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) @@ -219,7 +219,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN), ) - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) @@ -240,7 +240,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.getSyncStrict(id = params.userWalletId) tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data) @@ -283,7 +283,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { ), ) - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null coEvery { @@ -308,7 +308,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.getSyncStrict(id = params.userWalletId) tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) @@ -337,7 +337,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { cause = ApiResponseError.TimeoutException(), ) as ApiResponse - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns defaultResponse coEvery { @@ -359,7 +359,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.getSyncStrict(id = params.userWalletId) tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) @@ -407,7 +407,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { ), ) - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null coEvery { @@ -432,7 +432,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.getSyncStrict(id = params.userWalletId) tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) @@ -463,7 +463,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { ), ) as ApiResponse - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns defaultResponse coEvery { @@ -485,7 +485,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.getSyncStrict(id = params.userWalletId) tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) userTokensSaver.push(userWalletId = params.userWalletId, response = defaultResponse) From 06d1f35002520665f8ebb2387b366ace33c00f2b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Feb 2026 17:12:27 +0300 Subject: [PATCH 037/106] Updated on 2026-08-14 --- features/onramp/impl/build.gradle.kts | 4 + .../DefaultAvailableSwapPairsComponent.kt | 4 +- .../SetNoAvailablePairsTransformerV2.kt | 4 + .../market/SwapMarketsListBatchFlowManager.kt | 243 ++++++++++++++++++ .../SwapMarketsTokenItemConverter.kt | 148 +++++++++++ .../market/state/SwapMarketState.kt | 26 ++ .../model/AvailableSwapPairsModel.kt | 85 ++++++ .../availablepairs/ui/SwapMarketsListItems.kt | 113 ++++++++ .../onramp/swap/ui/SwapSelectTokens.kt | 185 +++++++++---- .../onramp/tokenlist/entity/TokenListUM.kt | 12 +- .../SetLoadingAccountTokenListTransformer.kt | 8 + .../SetNothingToFoundStateTransformerV2.kt | 1 + .../UpdateAccountTokenListTransformer.kt | 4 + .../onramp/tokenlist/ui/OnrampTokenList.kt | 96 ++++++- 14 files changed, 877 insertions(+), 56 deletions(-) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/converter/SwapMarketsTokenItemConverter.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index 1e3239f200..d836fce188 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { /** Project - API */ implementation(projects.features.account.api) implementation(projects.features.onramp.api) + implementation(projects.features.swap.api) implementation(projects.features.swap.domain) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) @@ -30,6 +31,8 @@ dependencies { /** Project - Common */ implementation(projects.common.routing) implementation(projects.common.ui) + implementation(projects.common.uiMarkets) + implementation(projects.common.uiCharts) /** Project - Domain */ implementation(projects.domain.appCurrency) @@ -51,6 +54,7 @@ dependencies { implementation(projects.domain.appTheme) implementation(projects.domain.appTheme.models) implementation(projects.data.common) + implementation(projects.domain.markets) /** DI */ implementation(deps.hilt.android) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt index eea5740b23..456b6e8c30 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt @@ -7,7 +7,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.features.onramp.swap.availablepairs.model.AvailableSwapPairsModel import com.tangem.features.onramp.tokenlist.entity.TokenListUM -import com.tangem.features.onramp.tokenlist.ui.onrampTokenList +import com.tangem.features.onramp.tokenlist.ui.onrampSwapTokenList import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -25,7 +25,7 @@ internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor( get() = model.state override fun LazyListScope.content(uiState: TokenListUM, modifier: Modifier) { - onrampTokenList(state = uiState) + onrampSwapTokenList(state = uiState) } @AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt index 9ee61d4ba9..ac87e348b8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt @@ -26,6 +26,8 @@ internal class SetNoAvailablePairsTransformerV2( .createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText) override fun transform(prevState: TokenListUM): TokenListUM { + val totalTokensCount = accountList.values.sumOf { it.size } + return prevState.copy( availableItems = persistentListOf(), unavailableItems = persistentListOf(), @@ -45,6 +47,7 @@ internal class SetNoAvailablePairsTransformerV2( .toPersistentList(), ) }.toPersistentList(), + totalTokensCount = totalTokensCount, ) } else { TokenListUMData.TokenList( @@ -52,6 +55,7 @@ internal class SetNoAvailablePairsTransformerV2( unavailableConverter.convertList(cryptoCurrencies) .map(TokensListItemUM::Token) }.toPersistentList(), + totalTokensCount = totalTokensCount, ) }, isBalanceHidden = isBalanceHidden, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt new file mode 100644 index 0000000000..e25d1bcc4a --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt @@ -0,0 +1,243 @@ +package com.tangem.features.onramp.swap.availablepairs.market + +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.* +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.onramp.swap.availablepairs.market.converter.SwapMarketsTokenItemConverter +import com.tangem.pagination.Batch +import com.tangem.pagination.BatchAction +import com.tangem.pagination.PaginationStatus +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.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +@Suppress("LongParameterList") +internal class SwapMarketsListBatchFlowManager( + getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, + private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType, + private val currentAppCurrency: Provider, + private val currentSearchText: Provider, + private val modelScope: CoroutineScope, + private val dispatchers: CoroutineDispatcherProvider, +) { + private val actionsFlow = MutableSharedFlow>() + private val updateStateJob = JobHolder() + + private val batchFlow = getMarketsTokenListFlowUseCase( + batchingContext = TokenListBatchingContext( + actionsFlow = actionsFlow, + coroutineScope = modelScope, + ), + batchFlowType = batchFlowType, + ) + + private val resultBatches = MutableStateFlow(ResultBatches()) + private val uiBatches = resultBatches.map { it.uiBatches } + + val uiItems: StateFlow> + get() = uiBatches + .map { batches -> + batches.asSequence() + .map { it.data } + .flatten() + .toImmutableList() + } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = persistentListOf(), + ) + + val isInInitialLoadingErrorState = batchFlow.state + .map { it.status is PaginationStatus.InitialLoadingError } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + + val isSearchNotFoundState = batchFlow.state + .map { batchListState -> + currentSearchText().isNullOrEmpty().not() && + batchListState.status is PaginationStatus.EndOfPagination && + batchListState.data.isEmpty() + } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + + val totalCount: StateFlow = batchFlow.state + .map { it.totalCount } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + + init { + batchFlow.state + .map { it.data } + .distinctUntilChanged { a, b -> + a.size == b.size && + a.map { it.key } == b.map { it.key } && + a.map { it.data }.flatten() == b.map { it.data }.flatten() + } + .onEach { + coroutineScope { + launch { + updateState(it) + }.saveIn(updateStateJob) + } + } + .flowOn(dispatchers.default) + .launchIn(modelScope) + } + + private suspend fun updateState(newList: List>>, forceUpdate: Boolean = false) = + withContext(dispatchers.default) { + resultBatches.update { resultBatches -> + val items = resultBatches.uiBatches + val previousList = resultBatches.processedItems + + val converter = SwapMarketsTokenItemConverter(appCurrency = currentAppCurrency()) + + if (newList.isEmpty()) { + return@update ResultBatches(processedItems = emptyList()) + } + + val isInitialLoading = + forceUpdate || previousList.isNullOrEmpty() || newList.first().key != previousList.first().key + + val outItems = if (isInitialLoading) { + newList.map { batch -> + Batch( + key = batch.key, + data = converter.convertList(batch.data), + ) + } + } else { + if (previousList.size != newList.size) { + val keysToAdd = newList.map { it.key }.subtract(previousList.map { it.key }.toSet()) + val newBatches = newList.filter { keysToAdd.contains(it.key) } + + items + newBatches.map { batch -> + Batch( + key = batch.key, + data = converter.convertList(batch.data), + ) + } + } else { + items.mapIndexed { batchIndex, batch -> + val prevBatch = previousList[batchIndex] + val newBatch = newList[batchIndex] + if (prevBatch == newBatch) return@mapIndexed batch + + Batch( + key = batch.key, + data = batch.data.mapIndexed { index, marketsListItemUM -> + val prevItem = prevBatch.data.getOrNull(index) + val newItem = newBatch.data.getOrNull(index) + if (prevItem != null && newItem != null) { + converter.update(prevItem, marketsListItemUM, newItem) + } else { + newItem?.let { converter.convert(it) } ?: marketsListItemUM + } + }, + ) + } + } + } + + currentCoroutineContext().ensureActive() + + ResultBatches( + uiBatches = outItems, + processedItems = newList, + ) + } + } + + fun reload(searchText: String? = null) { + modelScope.launch { + resultBatches.value = ResultBatches() + actionsFlow.emit( + BatchAction.Reload( + requestParams = TokenMarketListConfig( + fiatPriceCurrency = currentAppCurrency().code, + searchText = if (currentSearchText() == null) { + null + } else { + searchText ?: currentSearchText() + }, + priceChangeInterval = TokenMarketListConfig.Interval.H24, + order = TokenMarketListConfig.Order.ByRating, + ), + ), + ) + } + } + + fun loadMore() { + modelScope.launch { + actionsFlow.emit(BatchAction.LoadMore()) + } + } + + fun loadCharts(batchKeys: Set) { + if (batchKeys.isEmpty()) return + + modelScope.launch { + val currentData = batchFlow.state.value.data + val alreadyLoadedChartsBatchKeys = currentData + .filter { batch -> + val first = batch.data.firstOrNull() ?: return@filter false + first.tokenCharts.h24 != null + } + .map { it.key } + .toSet() + + val batchesKeysToLoad = batchKeys.minus(alreadyLoadedChartsBatchKeys) + + if (batchesKeysToLoad.isNotEmpty()) { + actionsFlow.emit( + BatchAction.UpdateBatches( + keys = batchesKeysToLoad, + updateRequest = TokenMarketUpdateRequest.UpdateChart( + interval = TokenMarketListConfig.Interval.H24, + currency = currentAppCurrency().code, + ), + async = true, + operationId = batchesKeysToLoad.toString() + "h24", + ), + ) + } + } + } + + fun getBatchKeysByItemIds(ids: List): Set { + val currentData = batchFlow.state.value.data + + return currentData + .filter { d -> d.data.any { ids.contains(it.id) } } + .map { it.key } + .toSet() + } + + private data class ResultBatches( + val uiBatches: List>> = emptyList(), + val processedItems: List>>? = null, + ) +} \ No newline at end of file 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 new file mode 100644 index 0000000000..6ea8ec9f3c --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/converter/SwapMarketsTokenItemConverter.kt @@ -0,0 +1,148 @@ +package com.tangem.features.onramp.swap.availablepairs.market.converter + +import com.tangem.common.ui.charts.state.MarketChartData +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.core.ui.R +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.compact +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.core.ui.format.bigdecimal.price +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarket +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal +import java.math.RoundingMode + +internal class SwapMarketsTokenItemConverter( + private val appCurrency: AppCurrency, +) : Converter { + + private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(shouldFormatAxis = false) + + override fun convert(value: TokenMarket): MarketsListItemUM { + return MarketsListItemUM( + id = value.id, + name = value.name, + currencySymbol = value.symbol, + ratingPosition = value.marketRating?.toString(), + marketCap = value.getMarketCap(), + iconUrl = value.imageUrlLarge, + price = value.getCurrentPrice(), + trendPercentText = value.getTrendPercent(), + trendType = value.getTrendType(), + chartData = value.getChartData(), + isUnder100kMarketCap = value.isUnderMarketCapLimit, + stakingRate = value.yieldRate?.format { percent() }?.let { + resourceReference(R.string.markets_apy_placeholder, wrappedList(it)) + }, + updateTimestamp = value.updateTimestamp, + ) + } + + fun convertList(items: List): List = items.map(::convert) + + fun update(prev: TokenMarket, prevUI: MarketsListItemUM, new: TokenMarket): MarketsListItemUM { + require(prev.id == new.id) { + "Ids is not the same during update TokenMarket item: previousItem[${prev.id}] != newItem[${new.id}]" + } + + return prevUI.copy( + name = new.name, + currencySymbol = new.symbol, + ratingPosition = new.marketRating?.toString(), + marketCap = ifChanged(prev.marketCap, new.marketCap, prevUI.marketCap) { new.getMarketCap() }, + iconUrl = new.imageUrlLarge, + price = ifChanged(prev = prev.tokenQuotesShort, new = new.tokenQuotesShort, prevR = prevUI.price) { + new.getCurrentPrice(prev = prev) + }, + trendPercentText = ifChanged( + prev.tokenQuotesShort, + new.tokenQuotesShort, + prevUI.trendPercentText, + ) { new.getTrendPercent() }, + trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() }, + chartData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chartData) { new.getChartData() }, + ) + } + + private inline fun ifChanged(prev: T, new: T, prevR: R, force: Boolean = false, change: (T) -> R): R { + return if (force || prev != new) change(new) else prevR + } + + private fun TokenMarket.getMarketCap(): String? { + val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null + + return value.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).compact( + threeDigitsMethod = true, + ) + } + } + + private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price { + val prevPrice = prev?.tokenQuotesShort?.currentPrice + + val priceText = tokenQuotesShort.currentPrice.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).price() + } + + val changeType = if (prevPrice != null) { + if (tokenQuotesShort.currentPrice > prevPrice) { + PriceChangeType.UP + } else { + PriceChangeType.DOWN + } + } else { + null + } + + return MarketsListItemUM.Price( + text = priceText, + changeType = changeType, + ) + } + + private fun TokenMarket.getChartData(): MarketChartRawData? { + val chart = tokenCharts.h24 + + return chart?.let { ct -> + priceAndTimePointValuesConverter.convert( + MarketChartData.Data( + y = ct.priceY.toImmutableList(), + x = ct.timeStamps.map { it.toBigDecimal() }.toImmutableList(), + ).sorted(), + ) + } + } + + @Suppress("MagicNumber") + private fun TokenMarket.getTrendType(): PriceChangeType { + val percent = tokenQuotesShort.h24ChangePercent + val scaled = percent?.setScale(4, RoundingMode.HALF_UP) + return when { + scaled == null -> PriceChangeType.NEUTRAL + scaled > BigDecimal.ZERO -> PriceChangeType.UP + scaled < BigDecimal.ZERO -> PriceChangeType.DOWN + else -> PriceChangeType.NEUTRAL + } + } + + private fun TokenMarket.getTrendPercent(): String { + val percent = tokenQuotesShort.h24ChangePercent + return percent.format { percent() } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt new file mode 100644 index 0000000000..bcd1c46b86 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt @@ -0,0 +1,26 @@ +package com.tangem.features.onramp.swap.availablepairs.market.state + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.domain.models.currency.CryptoCurrency +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class SwapMarketState { + + data class Content( + val items: ImmutableList, + val total: Int, + val loadMore: () -> Unit, + val onItemClick: (MarketsListItemUM) -> Unit, + val visibleIdsChanged: (List) -> Unit, + ) : SwapMarketState() + + data object Loading : SwapMarketState() + + data class LoadingError( + val onRetryClicked: () -> Unit, + ) : SwapMarketState() + + data object SearchNothingFound : SwapMarketState() +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index d862519a45..84d7dc9a8f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -18,6 +18,7 @@ import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrency @@ -30,10 +31,13 @@ import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.feature.swap.domain.models.domain.SwapPairLeast import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent +import com.tangem.features.swap.SwapFeatureToggles import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetLoadingTokenItemsTransformer import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformer import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformerV2 +import com.tangem.features.onramp.swap.availablepairs.market.SwapMarketsListBatchFlowManager +import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM import com.tangem.features.onramp.swap.entity.AccountCurrencyUM import com.tangem.features.onramp.tokenlist.entity.TokenListUM @@ -43,6 +47,7 @@ import com.tangem.features.onramp.tokenlist.entity.transformer.* import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.flow.* @@ -64,6 +69,8 @@ internal class AvailableSwapPairsModel @Inject constructor( private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val accountsFeatureToggles: AccountsFeatureToggles, + private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, + private val swapFeatureToggles: SwapFeatureToggles, getWalletsUseCase: GetWalletsUseCase, ) : Model() { @@ -76,6 +83,22 @@ internal class AvailableSwapPairsModel @Inject constructor( private val accountListFlow = getAccountListUseCaseFlow() private val availablePairsByNetworkFlow = MutableStateFlow>(emptyMap()) + private val selectedAppCurrencyFlow: StateFlow = getSelectedAppCurrencyUseCase.invokeOrDefault() + .stateIn(scope = modelScope, started = SharingStarted.Eagerly, initialValue = AppCurrency.Default) + private val searchQueryStateForMarkets = MutableStateFlow("") + private val visibleMarketItemIds = MutableStateFlow>(emptyList()) + + private val searchMarketsListManager by lazy { + SwapMarketsListBatchFlowManager( + getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, + currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, + currentSearchText = Provider { searchQueryStateForMarkets.value }, + modelScope = modelScope, + dispatchers = dispatchers, + ) + } + init { if (accountsFeatureToggles.isFeatureEnabled) { subscribeOnUpdateStateV2() @@ -85,6 +108,11 @@ internal class AvailableSwapPairsModel @Inject constructor( initializeSearchBarCallbacks() subscribeOnAvailablePairsUpdates() + + if (swapFeatureToggles.isMarketListFeatureEnabled) { + subscribeOnMarketsUpdates() + subscribeOnVisibleMarketItems() + } } private fun getTokenListUseCaseFlow(): SharedFlow> { @@ -421,6 +449,8 @@ internal class AvailableSwapPairsModel @Inject constructor( tokenListUMController.update(transformer = UpdateSearchQueryTransformer(newQuery)) searchManager.update(newQuery) + + searchQueryStateForMarkets.value = newQuery } } @@ -482,4 +512,59 @@ internal class AvailableSwapPairsModel @Inject constructor( network = currency.network.backendId, ) } + + @Suppress("LongMethod") + private fun subscribeOnMarketsUpdates() { + combine( + flow = searchQueryStateForMarkets + .onEach { searchQuery -> + if (searchQuery.isNotEmpty()) { + searchMarketsListManager.reload(searchQuery) + } + }, + flow2 = searchMarketsListManager.uiItems, + flow3 = searchMarketsListManager.isInInitialLoadingErrorState, + flow4 = searchMarketsListManager.isSearchNotFoundState, + flow5 = searchMarketsListManager.totalCount, + ) { searchQuery, uiItems, isError, isSearchNotFound, total -> + when { + searchQuery.isEmpty() -> { + visibleMarketItemIds.value = emptyList() + null + } + isError -> SwapMarketState.LoadingError( + onRetryClicked = { searchMarketsListManager.reload(searchQuery) }, + ) + isSearchNotFound -> SwapMarketState.SearchNothingFound + uiItems.isEmpty() -> SwapMarketState.Loading + else -> SwapMarketState.Content( + items = uiItems, + loadMore = { searchMarketsListManager.loadMore() }, + onItemClick = {}, + visibleIdsChanged = { visibleMarketItemIds.value = it }, + total = total ?: uiItems.size, + ) + } + } + .distinctUntilChanged() + .onEach { marketsState -> + tokenListUMController.update { it.copy(marketsState = marketsState) } + } + .flowOn(dispatchers.main) + .launchIn(modelScope) + } + + private fun subscribeOnVisibleMarketItems() { + modelScope.launch { + visibleMarketItemIds.mapNotNull { rawIds -> + if (rawIds.isNotEmpty()) { + searchMarketsListManager.getBatchKeysByItemIds(rawIds) + } else { + null + } + }.distinctUntilChanged().collectLatest { visibleBatchKeys -> + searchMarketsListManager.loadCharts(visibleBatchKeys) + } + } + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt new file mode 100644 index 0000000000..b8e098fa31 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt @@ -0,0 +1,113 @@ +package com.tangem.features.onramp.swap.availablepairs.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +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.UnableToLoadData +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState + +private const val LOADING_PLACEHOLDERS_COUNT = 20 + +internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) { + item(key = "markets_title") { + val totalCount = (state as? SwapMarketState.Content)?.total + Text( + text = buildAnnotatedString { + append(stringResourceSafe(R.string.markets_common_title)) + if (totalCount != null) { + withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { + append(" $totalCount") + } + } + }, + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16) + .padding(top = TangemTheme.dimens.spacing24, bottom = TangemTheme.dimens.spacing12), + ) + } + + when (state) { + is SwapMarketState.Loading -> { + items(count = LOADING_PLACEHOLDERS_COUNT, key = { "market_placeholder_$it" }) { + MarketsListItemPlaceholder() + } + } + is SwapMarketState.LoadingError -> { + item(key = "market_loading_error") { + LoadingErrorItem( + modifier = Modifier.fillParentMaxWidth(), + onTryAgain = state.onRetryClicked, + ) + } + } + SwapMarketState.SearchNothingFound -> { + item(key = "market_not_found") { + SearchNothingFoundText( + modifier = Modifier.fillParentMaxWidth(), + ) + } + } + is SwapMarketState.Content -> { + itemsIndexed( + items = state.items, + key = { _, item -> item.getComposeKey() }, + ) { index, item -> + MarketsListItem( + model = item, + onClick = { state.onItemClick(item) }, + modifier = Modifier.roundedShapeItemDecoration( + currentIndex = index, + lastIndex = state.items.lastIndex, + backgroundColor = TangemTheme.colors.background.action, + ), + ) + } + } + } +} + +@Composable +private fun LoadingErrorItem(onTryAgain: () -> Unit, modifier: Modifier = Modifier) { + Box( + modifier + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing12, + ), + contentAlignment = Alignment.Center, + ) { + UnableToLoadData(onRetryClick = onTryAgain) + } +} + +@Composable +private fun SearchNothingFoundText(modifier: Modifier = Modifier) { + Box( + modifier = modifier.padding(TangemTheme.dimens.spacing16), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResourceSafe(R.string.markets_search_token_no_result_title), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt index 346d2b498f..5d3e457cc8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt @@ -8,24 +8,33 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState 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.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent +import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState import com.tangem.features.onramp.swap.entity.ExchangeCardUM import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent import com.tangem.features.onramp.tokenlist.entity.TokenListUM +private const val LOAD_MORE_BUFFER = 25 + /** * Swap select tokens * @@ -49,8 +58,8 @@ internal fun SwapSelectTokens( BackHandler(onBack = state.onBackClick) val nestedScrollConnection = rememberHideKeyboardNestedScrollConnection() - val lazyListState = rememberLazyListState() + LazyColumn( modifier = modifier .nestedScroll(nestedScrollConnection) @@ -60,62 +69,146 @@ internal fun SwapSelectTokens( state = lazyListState, contentPadding = PaddingValues(bottom = 8.dp), ) { - stickyHeader(key = "header") { - AppBarWithBackButton( - onBackClick = state.onBackClick, - text = stringResourceSafe(id = R.string.common_swap), - iconRes = R.drawable.ic_close_24, - containerColor = TangemTheme.colors.background.secondary, - ) - } + swapSelectTokensContent( + state = state, + selectFromTokenListComponent = selectFromTokenListComponent, + selectFromTokenListState = selectFromTokenListState, + selectToTokenListComponent = selectToTokenListComponent, + selectToTokenListState = selectToTokenListState, + ) + } - item(key = "exchange_from", contentType = "exchange_from") { + ScrollToTopEffect(state = state, lazyListState = lazyListState) + + MarketsHandlers( + state = state, + selectToTokenListState = selectToTokenListState, + lazyListState = lazyListState, + ) +} + +@OptIn(ExperimentalFoundationApi::class) +private fun LazyListScope.swapSelectTokensContent( + state: SwapSelectTokensUM, + selectFromTokenListComponent: OnrampTokenListComponent, + selectFromTokenListState: TokenListUM, + selectToTokenListComponent: AvailableSwapPairsComponent, + selectToTokenListState: TokenListUM, +) { + stickyHeader(key = "header") { + AppBarWithBackButton( + onBackClick = state.onBackClick, + text = stringResourceSafe(id = R.string.common_swap), + iconRes = R.drawable.ic_close_24, + containerColor = TangemTheme.colors.background.secondary, + ) + } + + item(key = "exchange_from", contentType = "exchange_from") { + ExchangeCard( + state = state.exchangeFrom, + isBalanceHidden = state.isBalanceHidden, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 8.dp, bottom = 12.dp) + .animateItem(), + ) + } + + if (state.exchangeFrom is ExchangeCardUM.Empty) { + with(selectFromTokenListComponent) { + content(uiState = selectFromTokenListState, modifier = Modifier) + } + } + + if (state.exchangeFrom is ExchangeCardUM.Filled) { + exchangeToSection( + state = state, + selectToTokenListComponent = selectToTokenListComponent, + selectToTokenListState = selectToTokenListState, + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +private fun LazyListScope.exchangeToSection( + state: SwapSelectTokensUM, + selectToTokenListComponent: AvailableSwapPairsComponent, + selectToTokenListState: TokenListUM, +) { + item(key = "exchange_to", contentType = "exchange_to") { + if (selectToTokenListState.warning != NotificationUM.Warning.SwapNoAvailablePair) { ExchangeCard( - state = state.exchangeFrom, + state = state.exchangeTo, isBalanceHidden = state.isBalanceHidden, modifier = Modifier .padding(horizontal = 16.dp) - .padding(top = 8.dp, bottom = 12.dp) + .padding(bottom = 12.dp) .animateItem(), ) } - - if (state.exchangeFrom is ExchangeCardUM.Empty) { - with(selectFromTokenListComponent) { - content( - uiState = selectFromTokenListState, - modifier = Modifier, - ) - } - } - - if (state.exchangeFrom is ExchangeCardUM.Filled) { - item(key = "exchange_to", contentType = "exchange_to") { - if (selectToTokenListState.warning != NotificationUM.Warning.SwapNoAvailablePair) { - ExchangeCard( - state = state.exchangeTo, - isBalanceHidden = state.isBalanceHidden, - modifier = Modifier - .padding(horizontal = 16.dp) - .padding(bottom = 12.dp) - .animateItem(), - ) - } - } - - if (state.exchangeTo is ExchangeCardUM.Empty) { - with(selectToTokenListComponent) { - content( - uiState = selectToTokenListState, - modifier = Modifier.padding(horizontal = 16.dp), - ) - } - } - } } - // scroll to top after "from" token selection + if (state.exchangeTo is ExchangeCardUM.Empty) { + with(selectToTokenListComponent) { + content(uiState = selectToTokenListState, modifier = Modifier.padding(horizontal = 16.dp)) + } + } +} + +@Composable +private fun ScrollToTopEffect(state: SwapSelectTokensUM, lazyListState: LazyListState) { LaunchedEffect(state.exchangeFrom !is ExchangeCardUM.Empty) { lazyListState.scrollToItem(index = 0) } +} + +@Composable +private fun MarketsHandlers( + state: SwapSelectTokensUM, + selectToTokenListState: TokenListUM, + lazyListState: LazyListState, +) { + // Markets for "to" token list + if (state.exchangeFrom is ExchangeCardUM.Filled && state.exchangeTo is ExchangeCardUM.Empty) { + MarketsPaginationHandler( + marketsState = selectToTokenListState.marketsState, + lazyListState = lazyListState, + ) + } +} + +@Composable +private fun MarketsPaginationHandler(marketsState: SwapMarketState?, lazyListState: LazyListState) { + (marketsState as? SwapMarketState.Content)?.let { content -> + VisibleItemsTracker(lazyListState = lazyListState, marketState = content) + + InfiniteListHandler( + listState = lazyListState, + buffer = LOAD_MORE_BUFFER, + triggerLoadMoreCheckOnItemsCountChange = true, + onLoadMore = remember(content) { + { + content.loadMore() + true + } + }, + ) + } +} + +@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) + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt index 3f88743a05..04f7008ff8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt @@ -3,6 +3,7 @@ package com.tangem.features.onramp.tokenlist.entity import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState import kotlinx.collections.immutable.ImmutableList /** @@ -12,6 +13,7 @@ import kotlinx.collections.immutable.ImmutableList * @property availableItems available items (search bar, header, tokens) * @property unavailableItems unavailable items (header, tokens) * @property isBalanceHidden flag that indicates if balance should be hidden + * @property marketsState markets list state (null when markets should not be shown) * [REDACTED_AUTHOR] */ @@ -22,16 +24,24 @@ internal data class TokenListUM( val tokensListData: TokenListUMData, val isBalanceHidden: Boolean, val warning: NotificationUM? = null, + val marketsState: SwapMarketState? = null, ) internal sealed interface TokenListUMData { + + val totalTokensCount: Int + data class AccountList( val tokensList: ImmutableList, + override val totalTokensCount: Int, ) : TokenListUMData data class TokenList( val tokensList: ImmutableList, + override val totalTokensCount: Int, ) : TokenListUMData - data object EmptyList : TokenListUMData + data object EmptyList : TokenListUMData { + override val totalTokensCount: Int = 0 + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt index 48d7b7331d..b3a75c9a89 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt @@ -19,6 +19,12 @@ internal class SetLoadingAccountTokenListTransformer( private val accountListItemConverter = LoadingAccountTokenItemConverter(appCurrency) override fun transform(prevState: TokenListUM): TokenListUM { + val totalTokensCount = accountList.sumOf { account -> + when (account) { + is AccountStatus.CryptoPortfolio -> account.tokenList.flattenCurrencies().size + } + } + return prevState.copy( availableItems = persistentListOf(), unavailableItems = persistentListOf(), @@ -27,6 +33,7 @@ internal class SetLoadingAccountTokenListTransformer( tokensList = accountListItemConverter.convertList( accountList.filterIsInstance(), ).toPersistentList(), + totalTokensCount = totalTokensCount, ) } else { TokenListUMData.TokenList( @@ -37,6 +44,7 @@ internal class SetLoadingAccountTokenListTransformer( ) } }.toPersistentList(), + totalTokensCount = totalTokensCount, ) }, warning = null, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformerV2.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformerV2.kt index ddc0a2ac1f..422e445dff 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformerV2.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformerV2.kt @@ -23,6 +23,7 @@ internal class SetNothingToFoundStateTransformerV2( text = emptySearchMessageReference, ), ), + totalTokensCount = 0, ), isBalanceHidden = isBalanceHidden, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt index 51fe76f5af..8ccc9dabe0 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt @@ -40,12 +40,15 @@ internal class UpdateAccountTokenListTransformer( .createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText) override fun transform(prevState: TokenListUM): TokenListUM { + val totalTokensCount = accountList.sumOf { it.currencyList.size } + return prevState.copy( availableItems = persistentListOf(), unavailableItems = persistentListOf(), tokensListData = if (isAccountsMode) { TokenListUMData.AccountList( tokensList = accountListItemConverter.convertList(accountList).toPersistentList(), + totalTokensCount = totalTokensCount, ) } else { val tokensList = accountList.flatMap { (_, currencyList) -> @@ -66,6 +69,7 @@ internal class UpdateAccountTokenListTransformer( text = resourceReference(R.string.exchange_tokens_available_tokens_header), ), ) + tokensList, + totalTokensCount = totalTokensCount, ) } else { TokenListUMData.EmptyList diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index aefd164597..6cecf0ac0e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -3,18 +3,25 @@ package com.tangem.features.onramp.tokenlist.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH32 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 @@ -25,25 +32,80 @@ 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.conditional +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.utils.lazyListItemPosition +import com.tangem.features.onramp.swap.availablepairs.ui.swapMarketsListItems import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMData import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider import kotlinx.collections.immutable.ImmutableList /** - * Token list + * Token list for swap - automatically switches between normal and search mode with markets * - * @param state state + * @param state state * -[REDACTED_AUTHOR] + */ +internal fun LazyListScope.onrampSwapTokenList(state: TokenListUM) { + val isSearchMode = state.searchBarUM.query.isNotEmpty() && state.marketsState != null + if (isSearchMode) { + onrampTokenListWithMarkets(state = state) + } else { + onrampTokenList(state = state) + } +} + +/** + * Token list - normal mode (without markets) + * + * @param state state */ internal fun LazyListScope.onrampTokenList(state: TokenListUM) { val itemModifier = Modifier.padding(horizontal = 16.dp) + warningOrSearchBar(state = state, itemModifier = itemModifier) + + tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) + + tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden) + + tokensListData(state = state) +} + +/** + * Token list with markets - search mode + * + * @param state state + */ +private fun LazyListScope.onrampTokenListWithMarkets(state: TokenListUM) { + val itemModifier = Modifier.padding(horizontal = 16.dp) + + warningOrSearchBar(state = state, itemModifier = itemModifier) + + // Check if user has any assets to show + val hasAssets = state.availableItems.isNotEmpty() || + state.unavailableItems.isNotEmpty() || + state.tokensListData.totalTokensCount != 0 + + if (hasAssets) { + assetsTitle(count = state.tokensListData.totalTokensCount) + + tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) + + tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden) + + tokensListData(state = state) + + item { SpacerH32() } + } + + state.marketsState?.let(::swapMarketsListItems) +} + +private fun LazyListScope.warningOrSearchBar(state: TokenListUM, itemModifier: Modifier) { if (state.warning == null) { searchBarItem(searchBarUM = state.searchBarUM, modifier = itemModifier) } else { @@ -67,11 +129,9 @@ internal fun LazyListScope.onrampTokenList(state: TokenListUM) { } } } +} - tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) - - tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden) - +private fun LazyListScope.tokensListData(state: TokenListUM) { when (val list = state.tokensListData) { is TokenListUMData.AccountList -> list.tokensList.forEach { item -> portfolioTokensList( @@ -99,6 +159,28 @@ private fun LazyListScope.searchBarItem(searchBarUM: SearchBarUM, modifier: Modi } } +private fun LazyListScope.assetsTitle(count: Int) { + item(key = "assets_title") { + Text( + text = buildAnnotatedString { + append(stringResourceSafe(R.string.swap_your_assets_title)) + withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { + append(" $count") + } + }, + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing12, + ), + ) + } +} + private fun LazyListScope.tokensList(items: ImmutableList, isBalanceHidden: Boolean) { itemsIndexed( items = items, From 6ed1946ac3e34409bd311ca6c0b82bb8b09134b1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Feb 2026 19:50:01 +0400 Subject: [PATCH 038/106] Updated on 2026-08-14 --- data/manage-tokens/detekt-baseline-debug.xml | 9 - .../DefaultCustomTokensRepository.kt | 37 ++-- .../DefaultManageTokensRepository.kt | 10 +- .../managetokens/di/ManageTokensDataModule.kt | 10 +- .../utils/ManageTokensUpdateFetcher.kt | 6 +- .../tangem/data/visa/DefaultVisaRepository.kt | 10 +- .../data/visa/utils/VisaApiRequestMaker.kt | 21 +- data/wallets/build.gradle.kts | 10 +- .../wallets/DefaultWalletsPromoRepository.kt | 6 +- .../data/wallets/DefaultWalletsRepository.kt | 10 +- .../DefaultDerivationsRepository.kt | 31 ++- .../data/wallets/di/WalletsDataModule.kt | 10 +- .../hot/DefaultHotMapDerivationsRepository.kt | 6 +- .../wallets/DefaultWalletsRepositoryTest.kt | 4 +- .../DefaultDerivationsRepositoryTest.kt | 199 +++++++----------- .../wallets/UserWalletsListRepositoryExt.kt | 32 +++ 16 files changed, 191 insertions(+), 220 deletions(-) delete mode 100644 data/manage-tokens/detekt-baseline-debug.xml create mode 100644 domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepositoryExt.kt diff --git a/data/manage-tokens/detekt-baseline-debug.xml b/data/manage-tokens/detekt-baseline-debug.xml deleted file mode 100644 index a9b6d430bb..0000000000 --- a/data/manage-tokens/detekt-baseline-debug.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - MultilineLambdaItParameter:DefaultCustomTokensRepository.kt$DefaultCustomTokensRepository${ // TODO: refactor https://tangem.atlassian.net/browse/AND-10006\ if (it.isTestnet() || it in excludedBlockchains || it in hotWalletExcludedBlockchains) { return@mapNotNull null } networkFactory.create( blockchain = it, extraDerivationPath = null, userWallet = userWallet, ) } - MultilineLambdaItParameter:ManageTokensUpdateFetcher.kt$ManageTokensUpdateFetcher${ if (it.key == toUpdate[index].key) { Batch(it.key, updatedItems) } else { null } } - UnsafeCallOnNullableType:DefaultCustomTokensRepository.kt$DefaultCustomTokensRepository$coinNetwork.decimalCount!! - - diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index 1570f9a7c7..2617383978 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -13,11 +13,11 @@ import com.tangem.data.managetokens.utils.TokenAddressesConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.extensions.canHandleBlockchain import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains import com.tangem.domain.card.common.extensions.supportedBlockchains import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.managetokens.model.AddCustomTokenForm import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.repository.CustomTokensRepository @@ -32,7 +32,7 @@ import kotlinx.coroutines.withContext @Suppress("LongParameterList") internal class DefaultCustomTokensRepository( private val tangemTechApi: TangemTechApi, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val userTokensResponseStore: UserTokensResponseStore, private val walletManagersFacade: WalletManagersFacade, private val excludedBlockchains: ExcludedBlockchains, @@ -96,9 +96,8 @@ internal class DefaultCustomTokensRepository( networkId: Network.ID, derivationPath: Network.DerivationPath, ): CryptoCurrency.Token? = withContext(dispatchers.io) { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "User wallet [$userWalletId] not found while finding token" - } + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) + val network = requireNotNull( networkFactory.create( networkId = networkId, @@ -138,7 +137,7 @@ internal class DefaultCustomTokensRepository( rawId = CryptoCurrency.RawID(coin.id), name = coin.name, symbol = coin.symbol, - decimals = coinNetwork.decimalCount!!.toInt(), + decimals = requireNotNull(coinNetwork.decimalCount).toInt(), contractAddress = tokenAddress, ) } else { @@ -152,9 +151,7 @@ internal class DefaultCustomTokensRepository( networkId: Network.ID, derivationPath: Network.DerivationPath, ): CryptoCurrency.Coin { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "User wallet [$userWalletId] not found while creating coin" - } + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) val network = requireNotNull( networkFactory.create( networkId = networkId, @@ -189,9 +186,7 @@ internal class DefaultCustomTokensRepository( derivationPath: Network.DerivationPath, formValues: AddCustomTokenForm.Validated.All, ): CryptoCurrency.Token { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "User wallet [$userWalletId] not found while creating custom token" - } + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) val network = requireNotNull( networkFactory.create( networkId = networkId, @@ -217,6 +212,7 @@ internal class DefaultCustomTokensRepository( ) } + @Deprecated("Use ManageCryptoCurrenciesUseCase") override suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) = withContext(dispatchers.io) { val cryptoCurrency = when (currency) { @@ -273,20 +269,15 @@ internal class DefaultCustomTokensRepository( } override suspend fun getSupportedNetworks(userWalletId: UserWalletId): List = withContext(dispatchers.io) { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "User wallet [$userWalletId] not found while getting supported networks" - } - - when (userWallet) { + when (val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)) { is UserWallet.Hot -> { - Blockchain.entries.mapNotNull { - // TODO: refactor [REDACTED_JIRA]\ - if (it.isTestnet() || it in excludedBlockchains || it in hotWalletExcludedBlockchains) { - return@mapNotNull null - } + Blockchain.entries.mapNotNull { blockchain -> + // TODO: refactor [REDACTED_JIRA] + val isExcluded = blockchain in excludedBlockchains || blockchain in hotWalletExcludedBlockchains + if (blockchain.isTestnet() || isExcluded) return@mapNotNull null networkFactory.create( - blockchain = it, + blockchain = blockchain, extraDerivationPath = null, userWallet = userWallet, ) diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index 84eea67a90..4786abe9db 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -22,11 +22,11 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.orDefault import com.tangem.datasource.local.config.testnet.TestnetTokensStorage import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.TapWorkarounds.isTestCard import com.tangem.domain.card.common.extensions.* import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.managetokens.model.* import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.managetokens.repository.ManageTokensRepository @@ -46,7 +46,7 @@ import com.tangem.utils.coroutines.runSuspendCatching @Suppress("LongParameterList", "LargeClass") internal class DefaultManageTokensRepository( private val tangemTechApi: TangemTechApi, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val userTokenSaver: UserTokensSaver, private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher, private val userTokensResponseStore: UserTokensResponseStore, @@ -88,7 +88,7 @@ internal class DefaultManageTokensRepository( prefetchDistance = batchSize, batchSize = batchSize, subFetcher = { request, _, isFirstBatchFetching -> - val userWallet = request.params.userWalletId?.let(userWalletsStore::getSyncStrict) + val userWallet = request.params.userWalletId?.let(userWalletsListRepository::getSyncStrict) if (userWallet is UserWallet.Cold && userWallet.scanResponse.card.isTestCard) { when (val params = request.params) { @@ -438,7 +438,7 @@ internal class DefaultManageTokensRepository( userWalletId: UserWalletId, sourceNetwork: SourceNetwork, ): CurrencyUnsupportedState? { - val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = userWalletId) val blockchain = sourceNetwork.id.toBlockchain() return when (sourceNetwork) { @@ -452,7 +452,7 @@ internal class DefaultManageTokensRepository( rawNetworkId: String, isMainNetwork: Boolean, ): CurrencyUnsupportedState? { - val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = userWalletId) val blockchain = Blockchain.fromNetworkId(networkId = rawNetworkId) ?: error("Can not create blockchain with given networkId -> $rawNetworkId") return if (isMainNetwork) { diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index 97838b82c5..ea538bf3d2 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -11,8 +11,8 @@ import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.config.testnet.TestnetTokensStorage import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.domain.walletmanager.WalletManagersFacade @@ -31,7 +31,7 @@ internal object ManageTokensDataModule { @Singleton fun provideManageTokensRepository( tangemTechApi: TangemTechApi, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, manageTokensUpdateFetcher: ManageTokensUpdateFetcher, userTokensResponseStore: UserTokensResponseStore, userTokensSaver: UserTokensSaver, @@ -45,7 +45,7 @@ internal object ManageTokensDataModule { ): ManageTokensRepository { return DefaultManageTokensRepository( tangemTechApi = tangemTechApi, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, manageTokensUpdateFetcher = manageTokensUpdateFetcher, userTokensResponseStore = userTokensResponseStore, userTokenSaver = userTokensSaver, @@ -63,7 +63,7 @@ internal object ManageTokensDataModule { @Singleton fun provideCustomTokensRepository( tangemTechApi: TangemTechApi, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, userTokensResponseStore: UserTokensResponseStore, walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, @@ -73,7 +73,7 @@ internal object ManageTokensDataModule { ): CustomTokensRepository { return DefaultCustomTokensRepository( tangemTechApi = tangemTechApi, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, userTokensResponseStore = userTokensResponseStore, walletManagersFacade = walletManagersFacade, excludedBlockchains = excludedBlockchains, diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt index 66bfc00bc5..8778b13b4d 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt @@ -51,9 +51,9 @@ internal class ManageTokensUpdateFetcher @Inject constructor() : update { BatchUpdateResult.Success( - data = mapNotNull { - if (it.key == toUpdate[index].key) { - Batch(it.key, updatedItems) + data = mapNotNull { batch -> + if (batch.key == toUpdate[index].key) { + Batch(batch.key, updatedItems) } else { null } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index 2372e3639e..978d392878 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -16,8 +16,8 @@ import com.tangem.data.visa.config.VisaLibLoader import com.tangem.data.visa.utils.* import com.tangem.datasource.api.visa.VisaApi import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.requireColdWallet @@ -40,7 +40,7 @@ internal class DefaultVisaRepository @Inject constructor( private val visaLibLoader: VisaLibLoader, private val quotesFetcher: QuotesFetcher, private val cacheRegistry: CacheRegistry, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val dispatchers: CoroutineDispatcherProvider, private val visaApiRequestMaker: VisaApiRequestMaker, private val visaApi: VisaApi, @@ -176,7 +176,7 @@ internal class DefaultVisaRepository @Inject constructor( } } - private suspend fun makeAddress(userWalletId: UserWalletId): String { + private fun makeAddress(userWalletId: UserWalletId): String { if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoAddress() val userWallet = findVisaUserWallet(userWalletId) @@ -219,9 +219,7 @@ internal class DefaultVisaRepository @Inject constructor( } private fun findVisaUserWallet(userWalletId: UserWalletId): UserWallet { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "No user wallet found: $userWalletId" - } + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) if (!userWallet.requireColdWallet().scanResponse.cardTypesResolver.isVisaWallet()) { error("VISA wallet required: $userWalletId") } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt index a5f2592c55..ce95f24b90 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt @@ -7,8 +7,9 @@ import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest import com.tangem.datasource.api.visa.VisaApi -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.update import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.requireColdWallet @@ -23,7 +24,7 @@ import javax.inject.Inject typealias VisaAuthorizationHeader = String internal class VisaApiRequestMaker @Inject constructor( - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val visaAuthApi: VisaApi, private val accessCodeDataConverter: AccessCodeDataConverter, private val dispatcherProvider: CoroutineDispatcherProvider, @@ -47,11 +48,11 @@ internal class VisaApiRequestMaker @Inject constructor( val newTokens = runCatching { refreshAccessTokens(authTokens.refreshToken) - }.getOrElse { - if (it is ApiResponseError.HttpException && - it.code == ApiResponseError.HttpException.Code.UNAUTHORIZED + }.getOrElse { throwable -> + if (throwable is ApiResponseError.HttpException && + throwable.code == ApiResponseError.HttpException.Code.UNAUTHORIZED ) { - userWalletsStore.update(userWalletId) { userWallet -> + userWalletsListRepository.update(userWalletId) { userWallet -> userWallet.requireColdWallet().copy( scanResponse = userWallet.scanResponse.copy( // visaCardActivationStatus = VisaCardActivationStatus.RefreshTokenExpired, @@ -62,7 +63,7 @@ internal class VisaApiRequestMaker @Inject constructor( throw RefreshTokenExpiredException() } - userWalletsStore.update(userWalletId) { userWallet -> + userWalletsListRepository.update(userWalletId) { userWallet -> userWallet.requireColdWallet().copy( scanResponse = userWallet.scanResponse.copy( // visaCardActivationStatus = VisaCardActivationStatus.Activated( @@ -92,7 +93,7 @@ internal class VisaApiRequestMaker @Inject constructor( @Throws private fun getAuthTokens(userWalletId: UserWalletId): VisaAuthTokens { - val userWallet = findVisaUserWallet(userWalletId) + findVisaUserWallet(userWalletId) // val status = userWallet.requireColdWallet().scanResponse.visaCardActivationStatus // ?: error("Visa card activation status not found") val status: VisaCardActivationStatus = TODO("Fix visaCardActivationStatus retrieval") @@ -105,9 +106,7 @@ internal class VisaApiRequestMaker @Inject constructor( } private fun findVisaUserWallet(userWalletId: UserWalletId): UserWallet { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "No user wallet found: $userWalletId" - } + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) if (!userWallet.requireColdWallet().scanResponse.cardTypesResolver.isVisaWallet()) { error("VISA wallet required: $userWalletId") } diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index aafa1080db..512c474032 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -10,6 +10,10 @@ android { namespace = "com.tangem.data.wallet" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { implementation(projects.data.common) @@ -47,9 +51,7 @@ dependencies { implementation(deps.timber) /** tests */ + testImplementation(projects.test.core) testImplementation(projects.common.test) - testImplementation(deps.test.junit) - testImplementation(deps.test.coroutine) - testImplementation(deps.test.truth) - testImplementation(deps.test.mockk) + testRuntimeOnly(deps.test.junit5.engine) } \ No newline at end of file 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 de2bbc191e..5f50f1a7e5 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 @@ -9,7 +9,7 @@ import com.tangem.datasource.api.tangemTech.models.BindWalletsByReferralCodeBody import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.storeObject -import com.tangem.datasource.local.userwallet.UserWalletsStore +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 @@ -19,7 +19,7 @@ import timber.log.Timber internal class DefaultWalletsPromoRepository( private val appPreferencesStore: AppPreferencesStore, private val tangemTechApi: TangemTechApi, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val dispatchers: CoroutineDispatcherProvider, ) : WalletsPromoRepository { @@ -60,7 +60,7 @@ internal class DefaultWalletsPromoRepository( } private suspend fun bind(refcode: String, campaign: String?) { - val walletIds = userWalletsStore.userWalletsSync.map { it.walletId.stringValue } + val walletIds = userWalletsListRepository.userWallets.value.orEmpty().map { it.walletId.stringValue } val result = tangemTechApi.bindWalletsByReferralCode( body = BindWalletsByReferralCodeBody(walletIds = walletIds, refcode = refcode, campaign = campaign), 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 c9d66fa969..b6f5329aa5 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 @@ -24,8 +24,8 @@ import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus @@ -47,7 +47,7 @@ typealias SeedPhraseNotificationsStatuses = Map, private val dispatchers: CoroutineDispatcherProvider, private val authProvider: AuthProvider, @@ -151,7 +151,7 @@ internal class DefaultWalletsRepository( } private suspend fun fetchSeedPhraseNotificationStatus(userWalletId: UserWalletId) { - val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) + val userWallet = userWalletsListRepository.getSyncOrNull(id = userWalletId) if (userWallet != null && userWallet !is UserWallet.Cold) { updateNotificationVisibility(id = userWalletId, value = SeedPhraseNotificationsStatus.NOT_NEEDED) @@ -323,7 +323,7 @@ internal class DefaultWalletsRepository( } override suspend fun setWalletName(walletId: UserWalletId, walletName: String) = withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncOrNull(key = walletId) + val userWallet = userWalletsListRepository.getSyncOrNull(id = walletId) tangemTechApi.updateWallet( walletId = walletId.stringValue, @@ -332,7 +332,7 @@ internal class DefaultWalletsRepository( } override suspend fun upgradeWallet(walletId: UserWalletId) = withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncStrict(key = walletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = walletId) tangemTechApi.updateWallet( walletId = walletId.stringValue, 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 3a3dcf27ac..e355e23257 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 @@ -1,9 +1,9 @@ package com.tangem.data.wallets.derivations -import com.tangem.common.CompletionResult +import arrow.core.getOrElse import com.tangem.common.extensions.ByteArrayKey import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -16,16 +16,22 @@ import com.tangem.domain.wallets.usecase.BackendId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext +import timber.log.Timber import javax.inject.Inject internal class DefaultDerivationsRepository @Inject constructor( - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val hotDerivationsRepository: HotMapDerivationsRepository, private val coldDerivationsRepository: ColdMapDerivationsRepository, private val dispatchers: CoroutineDispatcherProvider, ) : DerivationsRepository { override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) { + if (currencies.isEmpty()) { + Timber.d("Nothing to derive") + return + } + derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network)) } @@ -34,7 +40,7 @@ internal class DefaultDerivationsRepository @Inject constructor( networkIds: List, accountIndex: DerivationIndex, ) { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) when (userWallet) { is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeysByNetworkIds(userWallet, networkIds) is UserWallet.Hot -> { @@ -46,7 +52,7 @@ internal class DefaultDerivationsRepository @Inject constructor( } override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List) { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) when (userWallet) { is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeysByNetworks(userWallet, networks) is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeysByNetworks(userWallet, networks) @@ -59,7 +65,7 @@ internal class DefaultDerivationsRepository @Inject constructor( userWalletId: UserWalletId, derivations: Map>, ): Map { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) return when (userWallet) { is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeys(userWallet, derivations) is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeys(userWallet, derivations) @@ -73,7 +79,7 @@ internal class DefaultDerivationsRepository @Inject constructor( userWalletId: UserWalletId, networksWithDerivationPath: Map, ): Boolean { - return when (val userWallet = userWalletsStore.getSyncStrict(userWalletId)) { + return when (val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)) { is UserWallet.Cold -> coldDerivationsRepository.hasMissedDerivations(userWallet, networksWithDerivationPath) is UserWallet.Hot -> hotDerivationsRepository.hasMissedDerivations(userWallet, networksWithDerivationPath) } @@ -88,14 +94,7 @@ internal class DefaultDerivationsRepository @Inject constructor( return@withContext // No update needed } - val updateResult = userWalletsStore.update( - userWalletId = newUserWallet.walletId, - update = { userWalletToUpdate -> newUserWallet }, - ) - - when (updateResult) { - is CompletionResult.Failure -> throw updateResult.error - is CompletionResult.Success -> updateResult.data - } + userWalletsListRepository.saveWithoutLock(userWallet = newUserWallet, canOverride = true) + .getOrElse { throw IllegalStateException("Unable to update user wallet: $it") } } } \ No newline at end of file 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 c5a72a7e5a..243b182f76 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 @@ -15,8 +15,8 @@ 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.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository @@ -41,7 +41,7 @@ internal object WalletsDataModule { fun providesWalletsRepository( appPreferencesStore: AppPreferencesStore, tangemTechApi: TangemTechApi, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, dispatchers: CoroutineDispatcherProvider, authProvider: AuthProvider, walletServerBinder: WalletServerBinder, @@ -52,7 +52,7 @@ internal object WalletsDataModule { return DefaultWalletsRepository( appPreferencesStore = appPreferencesStore, tangemTechApi = tangemTechApi, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()), dispatchers = dispatchers, authProvider = authProvider, @@ -74,13 +74,13 @@ internal object WalletsDataModule { fun provideWalletsPromoRepository( appPreferencesStore: AppPreferencesStore, tangemTechApi: TangemTechApi, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, dispatchers: CoroutineDispatcherProvider, ): WalletsPromoRepository { return DefaultWalletsPromoRepository( appPreferencesStore = appPreferencesStore, tangemTechApi = tangemTechApi, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, dispatchers = dispatchers, ) } 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 987dd65cc5..0484eb18f6 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 @@ -7,7 +7,7 @@ import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.common.network.NetworkFactory import com.tangem.data.wallets.derivations.MissedDerivationsFinder -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -23,7 +23,7 @@ import timber.log.Timber import javax.inject.Inject internal class DefaultHotMapDerivationsRepository @Inject constructor( - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val networkFactory: NetworkFactory, private val hotWalletAccessor: HotWalletAccessor, private val dispatchers: CoroutineDispatcherProvider, @@ -90,7 +90,7 @@ internal class DefaultHotMapDerivationsRepository @Inject constructor( // Get the updated user wallet from the store to ensure we have the latest data // in case it was modified during the derive operation - val updatedUserWallet = userWalletsStore.getSyncStrict(userWallet.walletId) as UserWallet.Hot + val updatedUserWallet = userWalletsListRepository.getSyncStrict(userWallet.walletId) as UserWallet.Hot val newKeys = result.responses.associate { ByteArrayKey(it.seedKey.publicKey) to ExtendedPublicKeysMap(it.publicKeys) } 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 30dd508d5b..71182a19c0 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 @@ -54,7 +54,7 @@ class DefaultWalletsRepositoryTest { repository = DefaultWalletsRepository( appPreferencesStore = appPreferenceStore, tangemTechApi = tangemTechApi, - userWalletsStore = mockk(), + userWalletsListRepository = mockk(), seedPhraseNotificationVisibilityStore = mockk(), dispatchers = dispatchers, authProvider = mockk(), @@ -222,7 +222,7 @@ class DefaultWalletsRepositoryTest { repository = DefaultWalletsRepository( appPreferencesStore = appPreferenceStore, tangemTechApi = tangemTechApi, - userWalletsStore = mockk(), + userWalletsListRepository = mockk(), seedPhraseNotificationVisibilityStore = mockk(), dispatchers = dispatchers, authProvider = authProvider, diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt index e9bccfd5a5..08a0df11ed 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt @@ -1,44 +1,36 @@ package com.tangem.data.wallets.derivations -import android.annotation.SuppressLint +import arrow.core.right import com.google.common.truth.Truth -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.common.CompletionResult import com.tangem.common.test.domain.card.MockScanResponseFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory -import com.tangem.data.common.network.NetworkFactory -import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.card.ScanCardException import com.tangem.domain.card.configs.GenericCardConfig -import com.tangem.domain.card.configs.MultiWalletCardConfig +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.operations.derivation.DerivationTaskResponse -import com.tangem.sdk.api.TangemSdkManager +import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk +import io.mockk.* import kotlinx.coroutines.test.runTest -import org.junit.Test +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance /** [REDACTED_AUTHOR] */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DefaultDerivationsRepositoryTest { - private val tangemSdkManager = mockk() - private val userWalletsStore = mockk() + private val userWalletsListRepository = mockk() + private val coldDerivationsRepository: ColdMapDerivationsRepository = mockk() + private val repository = DefaultDerivationsRepository( - userWalletsStore = userWalletsStore, - dispatchers = TestingCoroutineDispatcherProvider(), + userWalletsListRepository = userWalletsListRepository, hotDerivationsRepository = mockk(), - coldDerivationsRepository = DefaultColdMapDerivationsRepository( - tangemSdkManager = tangemSdkManager, - networkFactory = NetworkFactory(excludedBlockchains = ExcludedBlockchains()), - dispatchers = TestingCoroutineDispatcherProvider(), - ), + coldDerivationsRepository = coldDerivationsRepository, + dispatchers = TestingCoroutineDispatcherProvider(), ) private val defaultUserWalletId = UserWalletId("011") @@ -51,127 +43,94 @@ internal class DefaultDerivationsRepositoryTest { hasBackupError = false, ) + @AfterEach + fun tearDown() { + clearMocks(userWalletsListRepository, coldDerivationsRepository) + } + @Test fun `error if userWalletId not found`() = runTest { - coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } throws IllegalStateException() + val currencies = MockCryptoCurrencyFactory(defaultUserWallet).ethereum.let(::listOf) + coEvery { userWalletsListRepository.getSyncStrict(defaultUserWalletId) } throws IllegalStateException() runCatching { - repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) + repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = currencies) } .onSuccess { error("Should throws exception") } .onFailure { Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) } - coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } - coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) } - coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } + coVerify(exactly = 1) { userWalletsListRepository.getSyncStrict(defaultUserWalletId) } + coVerify(inverse = true) { + coldDerivationsRepository.derivePublicKeysByNetworks(any(), any()) + userWalletsListRepository.saveWithoutLock(any(), any()) + } } - @SuppressLint("CheckResult") - @Test - fun `success if card is not supported derivations`() = runTest { - coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns defaultUserWallet - - repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) - - runCatching { } - .onSuccess { Truth.assertThat(it) } - .onFailure { - error("Should returns success") - } - - coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } - coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) } - coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } - } - - @SuppressLint("CheckResult") @Test fun `success if currencies is empty`() = runTest { - val userWallet = defaultUserWallet.copy( - scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), - ) - coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet + repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) - runCatching { repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) } - .onSuccess { Truth.assertThat(it) } - .onFailure { error("Should returns success") } - - coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } - coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) } - coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } - } - - @SuppressLint("CheckResult") - @Test - fun `success if card already has derivations`() = runTest { - val userWallet = defaultUserWallet.copy( - scanResponse = MockScanResponseFactory.create( - cardConfig = MultiWalletCardConfig, - derivedKeys = DerivedKeysMocks.ethereumDerivedKeys, - ), - ) - - coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet - - runCatching { - repository.derivePublicKeys( - userWalletId = defaultUserWalletId, - currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf), - ) + coVerify(inverse = true) { + userWalletsListRepository.getSyncStrict(any()) + coldDerivationsRepository.derivePublicKeysByNetworks(any(), any()) + userWalletsListRepository.saveWithoutLock(any(), any()) } - .onSuccess { Truth.assertThat(it) } - .onFailure { error("Should returns success") } - - coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } - coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) } - coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } } @Test - fun `error if tangemSdkManager throws exception`() = runTest { - val userWallet = defaultUserWallet.copy( - scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), - ) - coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet - coEvery { tangemSdkManager.derivePublicKeys(null, any(), any()) } throws ScanCardException.UserCancelled() + fun `error if coldDerivationsRepository throws exception`() = runTest { + val currencies = MockCryptoCurrencyFactory(defaultUserWallet).ethereum.let(::listOf) + + coEvery { userWalletsListRepository.getSyncStrict(defaultUserWalletId) } returns defaultUserWallet + coEvery { + coldDerivationsRepository.derivePublicKeysByNetworks( + userWallet = defaultUserWallet, + networks = any(), + ) + } throws IllegalStateException() runCatching { - repository.derivePublicKeys( - userWalletId = defaultUserWalletId, - currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf), - ) + repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = currencies) } .onSuccess { error("Should throws exception") } - .onFailure { Truth.assertThat(it).isInstanceOf(ScanCardException.UserCancelled::class.java) } + .onFailure { Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) } - coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } - coVerify(exactly = 1) { tangemSdkManager.derivePublicKeys(null, any(), any()) } - coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } - } - - @SuppressLint("CheckResult") - @Test - fun `success case`() = runTest { - val userWallet = defaultUserWallet.copy( - scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), - ) - coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet - coEvery { tangemSdkManager.derivePublicKeys(null, any(), any()) } returns CompletionResult.Success( - DerivationTaskResponse(DerivedKeysMocks.ethereumDerivedKeys), - ) - coEvery { userWalletsStore.update(defaultUserWalletId, any()) } returns CompletionResult.Success(userWallet) - - runCatching { - repository.derivePublicKeys( - userWalletId = defaultUserWalletId, - currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf), + coVerifyOrder { + userWalletsListRepository.getSyncStrict(defaultUserWalletId) + coldDerivationsRepository.derivePublicKeysByNetworks( + userWallet = defaultUserWallet, + networks = currencies.map(CryptoCurrency.Coin::network), ) } - .onSuccess { Truth.assertThat(it) } - .onFailure { error("Should returns success but $it") } - coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } - coVerify(exactly = 1) { tangemSdkManager.derivePublicKeys(null, any(), any()) } - coVerify(exactly = 1) { userWalletsStore.update(defaultUserWalletId, any()) } + coVerify(inverse = true) { userWalletsListRepository.saveWithoutLock(any(), any()) } + } + + @Test + fun `success case`() = runTest { + val currencies = MockCryptoCurrencyFactory(defaultUserWallet).ethereum.let(::listOf) + val updatedWallet = defaultUserWallet.copy(cardsInWallet = setOf("AC01")) + + coEvery { userWalletsListRepository.getSyncStrict(defaultUserWalletId) } returns defaultUserWallet + coEvery { + coldDerivationsRepository.derivePublicKeysByNetworks( + userWallet = defaultUserWallet, + networks = currencies.map(CryptoCurrency.Coin::network), + ) + } returns updatedWallet + coEvery { + userWalletsListRepository.saveWithoutLock(updatedWallet, true) + } returns updatedWallet.right() + + repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = currencies) + + coVerifyOrder { + userWalletsListRepository.getSyncStrict(defaultUserWalletId) + coldDerivationsRepository.derivePublicKeysByNetworks( + userWallet = defaultUserWallet, + networks = currencies.map(CryptoCurrency.Coin::network), + ) + userWalletsListRepository.saveWithoutLock(updatedWallet, true) + } } } \ No newline at end of file diff --git a/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepositoryExt.kt b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepositoryExt.kt new file mode 100644 index 0000000000..d1474f7fd8 --- /dev/null +++ b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepositoryExt.kt @@ -0,0 +1,32 @@ +package com.tangem.domain.common.wallets + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.domain.common.wallets.error.SaveWalletError +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId + +/** + * Update user wallet by [userWalletId] and return updated wallet. + * + * @param userWalletId The ID of the user wallet to update. + * @param transform A function that takes the current user wallet and returns the updated version. + * @return Either containing the updated user wallet on success or an error if the update fails. + */ +suspend fun UserWalletsListRepository.update( + userWalletId: UserWalletId, + transform: suspend (UserWallet) -> UserWallet, +): Either = either { + val userWallet = userWallets.value?.find { it.walletId == userWalletId } + requireNotNull(userWallet) { "Unable to find user wallet with provided ID: $userWalletId" } + + val updatedUserWallet = transform(userWallet) + + saveWithoutLock( + userWallet = updatedUserWallet, + canOverride = true, + ) + .bind() + + updatedUserWallet +} \ No newline at end of file From aac48015b3cf4a85404fa5869ac6214330e1f3d7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Feb 2026 17:48:20 +0500 Subject: [PATCH 039/106] Updated on 2026-08-14 --- .../AccountPortfolioItemUMConverter.kt | 2 - .../common/ui/account/PortfolioSelectRow.kt | 2 +- .../converters/AmountAccountConverter.kt | 14 +++- .../producer/DefaultSingleAccountProducer.kt | 4 +- .../DefaultAccountsCRUDRepositoryTest.kt | 1 + .../swap/DefaultSwapTransactionRepository.kt | 8 +-- .../SavedSwapTransactionListConverter.kt | 4 +- .../sessions/DefaultWcSessionsManager.kt | 3 +- .../sign/WcSignUseCaseDelegate.kt | 1 - .../utils/WcNetworksConverter.kt | 7 +- .../account/models/AccountStatusList.kt | 12 ++-- .../account/producer/SingleAccountProducer.kt | 4 +- .../repository/AccountsCRUDRepository.kt | 2 +- .../account/supplier/SingleAccountSupplier.kt | 12 +++- .../DefaultSingleAccountStatusListProducer.kt | 1 + .../usecase/ArchiveCryptoPortfolioUseCase.kt | 4 +- .../ToggleTokenListGroupingUseCaseV2.kt | 4 +- .../AccountCryptoCurrencyStatusFinder.kt | 15 +++-- .../tangem/domain/models/account/Account.kt | 17 ++--- .../tangem/domain/models/account/AccountId.kt | 4 ++ .../domain/models/account/AccountStatus.kt | 23 +++++-- .../swap/models/SwapTransactionListModel.kt | 4 +- .../domain/swap/SwapTransactionRepository.kt | 4 +- .../usecase/SwapTransactionSentUseCase.kt | 4 +- .../domain/walletconnect/model/WcSession.kt | 2 +- .../account/PortfolioSelectorComponent.kt | 4 +- .../DefaultPortfolioSelectorController.kt | 33 +++++----- .../selector/PortfolioSelectorModel.kt | 1 + .../add/impl/model/AddToPortfolioModel.kt | 1 + .../AddToPortfolioPreselectedDataModel.kt | 2 + .../add/impl/model/AddTokenUiBuilder.kt | 7 +- .../impl/model/NewMarketsPortfolioDelegate.kt | 1 + .../model/CustomTokenSelectorModel.kt | 7 +- .../utils/list/ManageTokensUseCasesFacade.kt | 3 +- .../add/impl/model/AddToPortfolioModel.kt | 1 + .../add/impl/model/AddTokenUiBuilder.kt | 7 +- .../impl/model/NewMarketsPortfolioDelegate.kt | 1 + .../model/HotCryptoPortfolioDataLoader.kt | 1 + .../entity/OnrampAddTokenUiBuilder.kt | 3 +- .../LoadingAccountTokenItemConverter.kt | 4 +- .../LoadingTokenListItemConverter.kt | 7 +- .../SetLoadingTokenItemsTransformer.kt | 4 +- .../model/AvailableSwapPairsModel.kt | 8 ++- .../SetLoadingAccountTokenListTransformer.kt | 8 ++- .../tokenlist/model/OnrampTokenListModel.kt | 5 +- .../referral/domain/ReferralInteractorImpl.kt | 6 +- .../feature/referral/model/ReferralModel.kt | 1 + .../send/v2/send/DefaultSendComponent.kt | 1 + .../v2/send/analytics/SendAnalyticHelper.kt | 11 ++-- .../v2/send/confirm/SendConfirmComponent.kt | 2 +- .../features/send/v2/send/model/SendModel.kt | 2 +- .../amount/SendAmountComponentParams.kt | 6 +- .../amount/model/SendAmountModel.kt | 8 +-- .../destination/model/SendDestinationModel.kt | 52 ++++++++------- .../SendRecipientWalletListConverter.kt | 9 ++- .../ui/state/DestinationWalletUM.kt | 2 +- .../impl/amount/SwapAmountComponentParams.kt | 6 +- .../converter/SwapAmountFieldConverter.kt | 2 +- .../SwapAmountBalanceHiddenTransformer.kt | 2 +- .../SwapAmountPrimaryReadyStateTransformer.kt | 2 +- ...wapAmountSecondaryReadyStateTransformer.kt | 2 +- .../swap/v2/impl/common/ConfirmData.kt | 2 +- .../DefaultSendWithSwapComponent.kt | 8 ++- .../confirm/SendWithSwapConfirmComponent.kt | 2 +- .../confirm/model/SendWithSwapConfirmModel.kt | 1 + .../confirm/model/SwapTransactionSender.kt | 2 +- .../sendviaswap/model/SendWithSwapModel.kt | 2 +- .../com/tangem/features/swap/SwapComponent.kt | 2 +- .../swap/DefaultSwapTransactionRepository.kt | 8 +-- .../SavedSwapTransactionListConverter.kt | 9 +-- .../feature/swap/domain/SwapInteractor.kt | 16 ++--- .../feature/swap/domain/SwapInteractorImpl.kt | 64 +++++++++++-------- .../swap/domain/SwapTransactionRepository.kt | 4 +- .../domain/SavedSwapTransactionListModel.kt | 4 +- .../domain/models/ui/SwapTransactionState.kt | 4 +- .../utils/AccountItemsDelegate.kt | 5 +- .../utils/CryptoCurrenciesIdsResolver.kt | 8 ++- .../converter/TokenListToStateConverterV2.kt | 4 +- .../items/OrganizedTokenListConverter.kt | 16 ++--- .../domain/GetSingleWalletWarningsFactory.kt | 3 +- .../converter/TokenListStateConverter.kt | 2 + .../subscribers/BasicWalletSubscriber.kt | 9 ++- .../connections/model/WcPairModel.kt | 10 ++- .../WcSessionsAccountModeTransformer.kt | 3 +- .../converter/WcPortfolioNameDelegate.kt | 3 +- .../transaction/model/WcAddNetworkModel.kt | 1 - .../model/WcSendTransactionModel.kt | 1 - .../model/WcSignTransactionModel.kt | 1 - 88 files changed, 335 insertions(+), 239 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 6bb24f24e3..150bc43608 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 @@ -48,7 +48,6 @@ class AccountPortfolioItemUMConverter( ) UserWalletItemUM.Information.Loaded(text) } - is Account.Payment -> TODO("[REDACTED_JIRA]") } private fun getImageState(account: Account.CryptoPortfolio) = when (account) { @@ -56,7 +55,6 @@ class AccountPortfolioItemUMConverter( name = account.accountName.toUM().value, icon = CryptoPortfolioIconConverter.convert(account.icon), ) - is Account.Payment -> TODO("[REDACTED_JIRA]") } private fun getBalanceInfo(): UserWalletItemUM.Balance { diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt b/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt index e4575a197c..f45c250868 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt @@ -82,7 +82,7 @@ fun PortfolioSelectRow( @Immutable data class PortfolioSelectUM( - val icon: AccountIconUM.CryptoPortfolio?, + val icon: AccountIconUM?, val name: TextReference, val isAccountMode: Boolean, val isMultiChoice: Boolean, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountAccountConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountAccountConverter.kt index ea09230bd7..d347bf2908 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountAccountConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountAccountConverter.kt @@ -1,5 +1,6 @@ package com.tangem.common.ui.amountScreen.converters +import com.tangem.common.ui.account.AccountIconUM import com.tangem.common.ui.account.AccountTitleUM import com.tangem.common.ui.account.CryptoPortfolioIconConverter import com.tangem.common.ui.account.toUM @@ -11,12 +12,12 @@ class AmountAccountConverter( private val prefixText: TextReference, private val isAccountsMode: Boolean, private val walletTitle: TextReference, -) : Converter { - override fun convert(value: Account.CryptoPortfolio?): AccountTitleUM { +) : Converter { + override fun convert(value: Account?): AccountTitleUM { return if (value != null && isAccountsMode) { AccountTitleUM.Account( name = value.accountName.toUM().value, - icon = CryptoPortfolioIconConverter.convert(value.icon), + icon = getAccountIcon(account = value), prefixText = prefixText, ) } else { @@ -25,4 +26,11 @@ class AmountAccountConverter( ) } } + + private fun getAccountIcon(account: Account): AccountIconUM { + return when (account) { + is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(account.icon) + is Account.Payment -> AccountIconUM.Payment + } + } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountProducer.kt index 3471f7c173..b9bacb8d41 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountProducer.kt @@ -33,10 +33,10 @@ internal class DefaultSingleAccountProducer @AssistedInject constructor( private val dispatchers: CoroutineDispatcherProvider, ) : SingleAccountProducer { - override val fallback: Option + override val fallback: Option get() = none() - override fun produce(): Flow { + override fun produce(): Flow { return singleAccountListSupplier( params = SingleAccountListProducer.Params(userWalletId = params.accountId.userWalletId), ) diff --git a/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt index 0adcc20b4a..17a2f87acb 100644 --- a/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt @@ -19,6 +19,7 @@ import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.ArchivedAccount +import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.Account.CryptoPortfolio import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt index cbcb56f715..3734069fb2 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt @@ -60,8 +60,8 @@ internal class DefaultSwapTransactionRepository( userWalletId: UserWalletId, fromCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, transaction: SwapTransactionModel, ) { transaction.status?.let { swapTxList -> @@ -244,8 +244,8 @@ internal class DefaultSwapTransactionRepository( userWalletId: UserWalletId, fromCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, transactions: List, ): List { return addOrReplace( diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt index 552341da60..aa9d27b4b7 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt @@ -101,8 +101,8 @@ internal class SavedSwapTransactionListConverter( userWalletId: UserWalletId, fromCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, tokenTransactions: List, ) = SwapTransactionListDTO( userWalletId = userWalletId.stringValue, 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 848442eff1..8f37e4e4a1 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 @@ -9,6 +9,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.utils.* import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.walletconnect.WcAnalyticEvents @@ -116,7 +117,7 @@ internal class DefaultWcSessionsManager( val wcSessions = savedPending.plus(inStore).mapNotNull { storeSession -> val wallet = wallets.find { it.walletId == storeSession.walletId } ?: return@mapNotNull null val sdkSession = inSdk.find { it.topic == storeSession.topic } ?: return@mapNotNull null - val account = storeSession.accountId?.let { wcNetworksConverter.getAccount(it) } + val account = storeSession.accountId?.let { wcNetworksConverter.getAccount(it) } as? Account.CryptoPortfolio if (accountsFeatureToggles.isFeatureEnabled && account == null) { return@mapNotNull null } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt index 346ba94376..ebf56dafcf 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt @@ -5,7 +5,6 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.sign.SignStateConverter.toPreSign import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.SignStateConverter.toSigning -import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcRequestError import com.tangem.domain.walletconnect.model.WcRequestError.Companion.code diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt index 82a5b0beb2..d4b19d974a 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt @@ -13,6 +13,7 @@ import com.tangem.domain.account.supplier.SingleAccountSupplier 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.network.Network import com.tangem.domain.models.wallet.UserWallet @@ -146,10 +147,12 @@ internal class WcNetworksConverter @Inject constructor( .filterIsInstance().map(CryptoCurrency.Coin::network) } - private suspend fun getAccountStatus(accountId: AccountId): AccountStatus? { + private suspend fun getAccountStatus(accountId: AccountId): AccountStatus.CryptoPortfolio? { return singleAccountStatusListSupplier.getSyncOrNull( SingleAccountStatusListProducer.Params(accountId.userWalletId), - )?.accountStatuses?.find { it.account.accountId == accountId } + )?.accountStatuses + ?.filterCryptoPortfolio() + ?.find { it.account.accountId == accountId } } suspend fun getAccountNetworks(accountId: AccountId): List { diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt index 8309ed3403..cc3b4c4204 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt @@ -5,6 +5,7 @@ import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance 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.wallet.UserWalletId import kotlinx.serialization.Serializable @@ -33,13 +34,16 @@ data class AccountStatusList( ) { val mainAccount: AccountStatus.CryptoPortfolio - get() = accountStatuses.first { accountStatus -> - when (accountStatus) { - is AccountStatus.CryptoPortfolio -> accountStatus.account.isMainAccount + get() = accountStatuses + .filterCryptoPortfolio() + .first { accountStatus -> + when (accountStatus) { + is AccountStatus.CryptoPortfolio -> accountStatus.account.isMainAccount + } } - } as AccountStatus.CryptoPortfolio fun flattenCurrencies(): List = accountStatuses + .filterCryptoPortfolio() .map { accountStatus -> accountStatus.flattenCurrencies() } .flatten() diff --git a/domain/account/src/main/java/com/tangem/domain/account/producer/SingleAccountProducer.kt b/domain/account/src/main/java/com/tangem/domain/account/producer/SingleAccountProducer.kt index 541ce35936..76a318a696 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/producer/SingleAccountProducer.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/producer/SingleAccountProducer.kt @@ -5,10 +5,10 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId /** - * Produces a flow of [Account.CryptoPortfolio] for a single account identified by [Params.accountId]. + * Produces a flow of [Account] for a single account identified by [Params.accountId]. * The flow emits updates whenever the account's portfolio changes. */ -interface SingleAccountProducer : FlowProducer { +interface SingleAccountProducer : FlowProducer { data class Params(val accountId: AccountId) diff --git a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt index 59048e8fab..5e76e84653 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt @@ -28,7 +28,7 @@ interface AccountsCRUDRepository { * Retrieves a specific account by its unique identifier * * @param accountId the unique identifier of the account - * @return an [Option] containing the [Account.CryptoPortfolio] if found, or `Option.None` if not + * @return an [Option] containing the [Account] if found, or `Option.None` if not */ suspend fun getAccountSync(accountId: AccountId): Option diff --git a/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt b/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt index 527a32f557..2a87c8b14e 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt @@ -3,9 +3,12 @@ package com.tangem.domain.account.supplier import com.tangem.domain.account.producer.SingleAccountProducer import com.tangem.domain.core.flow.FlowCachingSupplier import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterIsInstance /** - * Supplies instances of [SingleAccountProducer] that produce flows of [Account.CryptoPortfolio] + * Supplies instances of [SingleAccountProducer] that produce flows of [Account] * for individual accounts. Each producer is uniquely identified by its [SingleAccountProducer.Params]. * * @property factory A factory to create instances of [SingleAccountProducer]. @@ -14,4 +17,9 @@ import com.tangem.domain.models.account.Account abstract class SingleAccountSupplier( override val factory: SingleAccountProducer.Factory, override val keyCreator: (SingleAccountProducer.Params) -> String, -) : FlowCachingSupplier() \ No newline at end of file +) : FlowCachingSupplier() { + + fun filterPaymentAccount(accountId: AccountId): Flow { + return invoke(params = SingleAccountProducer.Params(accountId)).filterIsInstance() + } +} \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt index 956cc9e4ce..db4baa88fa 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 @@ -294,6 +294,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo return map { accountStatus -> when (accountStatus) { is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance + is AccountStatus.Payment -> accountStatus.totalFiatBalance } } } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCase.kt index 7a0c57b6ac..de9a515a80 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCase.kt @@ -9,7 +9,7 @@ import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier 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.UserWalletId import com.tangem.domain.referral.ReferralRepository @@ -68,7 +68,7 @@ class ArchiveCryptoPortfolioUseCase( val account = accountStatusList.accountStatuses .asSequence() - .filterIsInstance() + .filterCryptoPortfolio() .firstOrNull { it.accountId == accountId } ensureNotNull(account) { diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2.kt index 3583eee3d2..32e1b28366 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2.kt @@ -48,10 +48,12 @@ class ToggleTokenListGroupingUseCaseV2( account.copy(tokenList = account.tokenList.reverseGroupType()) } + val updatedCryptoAccount = + updatedAccountList.firstOrNull { it is AccountStatus.CryptoPortfolio } as? AccountStatus.CryptoPortfolio accountStatusList.copy( accountStatuses = updatedAccountList, - groupType = when (updatedAccountList.firstOrNull()?.getCryptoTokenList()) { + groupType = when (updatedCryptoAccount?.tokenList) { is TokenList.GroupedByNetwork -> TokensGroupType.NETWORK is TokenList.Ungrouped -> TokensGroupType.NONE else -> accountStatusList.groupType diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/AccountCryptoCurrencyStatusFinder.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/AccountCryptoCurrencyStatusFinder.kt index dc33b171dd..fe5daf41d0 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/AccountCryptoCurrencyStatusFinder.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/AccountCryptoCurrencyStatusFinder.kt @@ -7,6 +7,7 @@ import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatuses import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.lib.crypto.derivation.AccountNodeRecognizer @@ -49,7 +50,7 @@ internal object AccountCryptoCurrencyStatusFinder { ): AccountCryptoCurrencyStatus? { return accountStatusList.getExpectedAccountStatuses(network) .asSequence() - .filterIsInstance() + .filterCryptoPortfolio() .mapNotNull { accountStatus -> val status = accountStatus.flattenCurrencies().firstOrNull { it.currency.id == currencyId } ?: return@mapNotNull null @@ -78,7 +79,7 @@ internal object AccountCryptoCurrencyStatusFinder { return accountStatusList.getExpectedAccountStatuses(networks) .asSequence() - .filterIsInstance() + .filterCryptoPortfolio() .associate { accountStatus -> val statuses = accountStatus.flattenCurrencies() .filter { it.currency.id in currencyIds } @@ -108,7 +109,7 @@ internal object AccountCryptoCurrencyStatusFinder { derivationPath = derivationPath, ) .asSequence() - .filterIsInstance() + .filterCryptoPortfolio() .mapNotNull { accountStatus -> val status = accountStatus.flattenCurrencies().firstOrNull { val currency = it.currency @@ -156,8 +157,8 @@ internal object AccountCryptoCurrencyStatusFinder { DerivationIndex.Main.value -> listOf(mainAccount) // currency only in the account with specific derivation index or in the main account else -> { - val accountStatus = accountStatuses.firstOrNull { - val cryptoPortfolio = it.account as? Account.CryptoPortfolio ?: return@firstOrNull false + val accountStatus = accountStatuses.firstOrNull { accountStatus -> + val cryptoPortfolio = accountStatus.account as? Account.CryptoPortfolio ?: return@firstOrNull false cryptoPortfolio.derivationIndex.value == possibleAccountIndex } @@ -172,8 +173,8 @@ internal object AccountCryptoCurrencyStatusFinder { if (possibleAccountIndexes.isEmpty()) return this@getExpectedAccountStatuses.accountStatuses - val accountStatuses = this@getExpectedAccountStatuses.accountStatuses.filter { - val cryptoPortfolio = it.account as? Account.CryptoPortfolio ?: return@filter false + val accountStatuses = this@getExpectedAccountStatuses.accountStatuses.filter { accountStatus -> + val cryptoPortfolio = accountStatus.account as? Account.CryptoPortfolio ?: return@filter false cryptoPortfolio.derivationIndex.value in possibleAccountIndexes } 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 851c25e463..017f431065 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 @@ -109,7 +109,7 @@ sealed interface Account { raise(AccountNameError(cause = it)) } - val derivationIndex = DerivationIndex(value = derivationIndex).getOrElse { + val index = DerivationIndex(value = derivationIndex).getOrElse { raise(DerivationIndexError(cause = it)) } @@ -117,7 +117,7 @@ sealed interface Account { accountId = accountId, accountName = accountName, icon = icon, - derivationIndex = derivationIndex, + derivationIndex = index, cryptoCurrencies = cryptoCurrencies, ) } @@ -175,11 +175,12 @@ sealed interface Account { } } - class Payment : Account { - override val accountId: AccountId - get() = TODO("Not yet implemented") - override val accountName: AccountName - get() = TODO("Not yet implemented") + @Serializable + data class Payment( + override val accountId: AccountId, + override val accountName: AccountName, + val cryptoCurrencies: List, + ) : Account { init { error("Not yet implemented") @@ -190,5 +191,5 @@ sealed interface Account { val Account.derivationIndex: DerivationIndex? get() = when (this) { is Account.CryptoPortfolio -> derivationIndex - is Account.Payment -> TODO("[REDACTED_JIRA]") + is Account.Payment -> null } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt index bffd56a6c4..0c65375595 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt @@ -71,5 +71,9 @@ data class AccountId private constructor( return AccountId(value = value, userWalletId = userWalletId) } + + fun forPaymentAccount(userWalletId: UserWalletId): AccountId { + return AccountId(value = "payment_$userWalletId", userWalletId = userWalletId) + } } } \ No newline at end of file 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 b04e872b58..31df2e7281 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,6 +1,7 @@ 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 @@ -33,13 +34,23 @@ sealed interface AccountStatus { override val account: Account.CryptoPortfolio, val tokenList: TokenList, val priceChangeLce: Lce, + ) : AccountStatus { + fun flattenCurrencies(): List { + return tokenList.flattenCurrencies() + } + } + + @Serializable + data class Payment( + override val account: Account.Payment, + val totalFiatBalance: TotalFiatBalance, ) : AccountStatus +} - fun flattenCurrencies(): List = when (this) { - is CryptoPortfolio -> tokenList.flattenCurrencies() - } +fun Iterable.filterCryptoPortfolio(): List { + return filterIsInstance() +} - fun getCryptoTokenList(): TokenList = when (this) { - is CryptoPortfolio -> tokenList - } +fun Sequence.filterCryptoPortfolio(): Sequence { + return filterIsInstance() } \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt index 89edac21b3..5c8e52d327 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt @@ -14,8 +14,8 @@ data class SwapTransactionListModel( val toCryptoCurrencyId: String, val fromCryptoCurrency: CryptoCurrency, val toCryptoCurrency: CryptoCurrency, - val fromAccount: Account.CryptoPortfolio?, - val toAccount: Account.CryptoPortfolio?, + val fromAccount: Account?, + val toAccount: Account?, val transactions: List, ) diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt index 7fe9292ea2..3e22c172a7 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt @@ -30,8 +30,8 @@ interface SwapTransactionRepository { userWalletId: UserWalletId, fromCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, transaction: SwapTransactionModel, ) diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt index 92fcc325e3..70c9811d57 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt @@ -22,8 +22,8 @@ class SwapTransactionSentUseCase( userWallet: UserWallet, fromCryptoCurrencyStatus: CryptoCurrencyStatus, toCryptoCurrencyStatus: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, swapDataTransactionModel: SwapDataTransactionModel, provider: ExpressProvider, txHash: String, diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt index 49ae45531a..cf3f4c9f37 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt @@ -8,7 +8,7 @@ import com.tangem.domain.models.wallet.UserWallet data class WcSession( val wallet: UserWallet, - val account: Account?, + val account: Account.CryptoPortfolio?, val networks: Set, val sdkModel: WcSdkSession, val securityStatus: CheckDAppResult, diff --git a/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt index 50c01a8d05..7d8627e929 100644 --- a/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt +++ b/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt @@ -60,5 +60,7 @@ interface PortfolioSelectorController { suspend fun isAccountModeSync(): Boolean fun selectAccount(accountId: AccountId?) - fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow?> + fun selectedAccountWithData( + portfolioFetcher: PortfolioFetcher, + ): Flow?> } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt b/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt index fe9a055c3f..c125505737 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt @@ -3,6 +3,7 @@ package com.tangem.features.account.selector 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.features.account.PortfolioFetcher import com.tangem.features.account.PortfolioSelectorController @@ -35,21 +36,23 @@ internal class DefaultPortfolioSelectorController @Inject constructor( _selectedAccount.tryEmit(accountId) } - override fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow?> = - combine( - flow = _selectedAccount, - flow2 = portfolioFetcher.data, - transform = { accountId, data -> - accountId ?: return@combine null - var result: Pair? = null + override fun selectedAccountWithData( + portfolioFetcher: PortfolioFetcher, + ): Flow?> = combine( + flow = _selectedAccount, + flow2 = portfolioFetcher.data, + transform = { accountId, data -> + accountId ?: return@combine null + var result: Pair? = null - data.balances.forEach { wallet, balance -> - val accountStatuses = balance.accountsBalance.accountStatuses - .find { accountId == it.account.accountId } - if (accountStatuses != null) result = balance.userWallet to accountStatuses - } + data.balances.forEach { wallet, balance -> + val accountStatuses = balance.accountsBalance.accountStatuses + .filterCryptoPortfolio() + .find { accountId == it.account.accountId } + if (accountStatuses != null) result = balance.userWallet to accountStatuses + } - return@combine result - }, - ) + return@combine result + }, + ) } \ 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 fc13e4df56..d7e351ff96 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,6 +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 } val accountItemUM = AccountPortfolioItemUMConverter( onClick = { selectorController.selectAccount(account.accountId) }, 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 a06d46878d..e234d1d2f6 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 @@ -325,6 +325,7 @@ internal class AddToPortfolioModel @Inject constructor( ): CryptoCurrency? { val accountIndex = when (val accountStatus = account.account) { is AccountStatus.CryptoPortfolio -> accountStatus.account.derivationIndex + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } return getTokenMarketCryptoCurrency( userWalletId = userWallet.walletId, 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 c6b6d1b45a..8beb2880d3 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 @@ -174,6 +174,7 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor( val availableToAddAccounts = accounts.mapNotNull { accountStatus -> val accountIndex = when (accountStatus) { is AccountStatus.CryptoPortfolio -> accountStatus.account.derivationIndex + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } val cryptoCurrency = getTokenMarketCryptoCurrency( @@ -220,6 +221,7 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor( ): CryptoCurrency? { val accountIndex = when (val accountStatus = account.account) { is AccountStatus.CryptoPortfolio -> accountStatus.account.derivationIndex + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } return getTokenMarketCryptoCurrency( userWalletId = userWallet.walletId, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenUiBuilder.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenUiBuilder.kt index 5d5c7e379e..2055a204ab 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenUiBuilder.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenUiBuilder.kt @@ -13,7 +13,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.iconResId import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.AccountStatus.* import com.tangem.features.feed.components.market.details.portfolio.add.SelectedNetwork import com.tangem.features.feed.components.market.details.portfolio.add.SelectedPortfolio import com.tangem.features.feed.components.market.details.portfolio.add.impl.AddTokenComponent @@ -36,7 +36,7 @@ internal class AddTokenUiBuilder @Inject constructor( } private fun createPortfolio(selectedPortfolio: SelectedPortfolio): PortfolioSelectUM { - val accountIcon: AccountIconUM.CryptoPortfolio? + val accountIcon: AccountIconUM? val portfolioName: TextReference when (selectedPortfolio.isAccountMode) { false -> { @@ -47,7 +47,8 @@ internal class AddTokenUiBuilder @Inject constructor( val accountStatus = selectedPortfolio.account.account portfolioName = accountStatus.account.accountName.toUM().value accountIcon = when (accountStatus) { - is AccountStatus.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon) + is CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon) + is Payment -> AccountIconUM.Payment } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/NewMarketsPortfolioDelegate.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/NewMarketsPortfolioDelegate.kt index 21a34d471c..2df91aab47 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/NewMarketsPortfolioDelegate.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/NewMarketsPortfolioDelegate.kt @@ -202,6 +202,7 @@ internal class NewMarketsPortfolioDelegate @AssistedInject constructor( val currencyId = status.currency.id.rawCurrencyId ?: return@filter false getTokenIdIfL2Network(currencyId.value) == currencyRawId.value } + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } return accountStatuses.map { accountStatus -> AccountWithAdded( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt index 2cbfe3dc7a..7aa77b2a2b 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt @@ -214,9 +214,10 @@ internal class CustomTokenSelectorModel @Inject constructor( val accounts = singleAccountStatusListSupplier(mode.userWalletId) .first().accountStatuses - val accountStatus = accounts.find { - when (it) { - is AccountStatus.CryptoPortfolio -> it.sameNodeAndNotMain() + val accountStatus = accounts.find { account -> + when (account) { + is AccountStatus.CryptoPortfolio -> account.sameNodeAndNotMain() + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt index c3fe0bf606..7bf8e67db0 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt @@ -12,6 +12,7 @@ import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.managetokens.model.ManageTokensListConfig import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId @@ -88,7 +89,7 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( val account = singleAccountSupplier.getSyncOrNull( params = SingleAccountProducer.Params(accountId = mode.accountId), - ) + ) as? Account.CryptoPortfolio ?: return IllegalStateException("Account not found").left() (account.cryptoCurrencies + added - removed).any { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt index 6b0fd6a169..142c66cf7a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -315,6 +315,7 @@ internal class AddToPortfolioModel @Inject constructor( ): CryptoCurrency? { val accountIndex = when (account.account) { is AccountStatus.CryptoPortfolio -> account.account.account.derivationIndex + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } return getTokenMarketCryptoCurrency( userWalletId = userWallet.walletId, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenUiBuilder.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenUiBuilder.kt index 107c7a8c3b..ec88d88216 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenUiBuilder.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenUiBuilder.kt @@ -13,7 +13,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.iconResId import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.AccountStatus.* import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.add.api.SelectedNetwork import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio @@ -36,7 +36,7 @@ internal class AddTokenUiBuilder @Inject constructor( } private fun createPortfolio(selectedPortfolio: SelectedPortfolio): PortfolioSelectUM { - val accountIcon: AccountIconUM.CryptoPortfolio? + val accountIcon: AccountIconUM? val portfolioName: TextReference when (selectedPortfolio.isAccountMode) { false -> { @@ -47,7 +47,8 @@ internal class AddTokenUiBuilder @Inject constructor( val accountStatus = selectedPortfolio.account.account portfolioName = accountStatus.account.accountName.toUM().value accountIcon = when (accountStatus) { - is AccountStatus.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon) + is CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon) + is Payment -> AccountIconUM.Payment } } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt index 286e1dc18e..17acbaf94a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt @@ -203,6 +203,7 @@ internal class NewMarketsPortfolioDelegate @AssistedInject constructor( val currencyId = status.currency.id.rawCurrencyId ?: return@filter false getTokenIdIfL2Network(currencyId.value) == currencyRawId.value } + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } return accountStatuses.map { accountStatus -> AccountWithAdded( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoPortfolioDataLoader.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoPortfolioDataLoader.kt index 961019b158..d49f018ad2 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoPortfolioDataLoader.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoPortfolioDataLoader.kt @@ -45,6 +45,7 @@ internal class HotCryptoPortfolioDataLoader @Inject constructor( val accountsWithHotCrypto = walletAccounts.accountStatuses.map { accountStatus -> val account: AccountStatus.CryptoPortfolio = when (accountStatus) { is AccountStatus.CryptoPortfolio -> accountStatus + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } val addedHotCrypto = mapOfAddedCurrencies[account.account].orEmpty() HotCryptoPortfolioData.Account( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddTokenUiBuilder.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddTokenUiBuilder.kt index a7b415212a..ac1f747c49 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddTokenUiBuilder.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddTokenUiBuilder.kt @@ -37,7 +37,7 @@ internal class OnrampAddTokenUiBuilder @Inject constructor( } private suspend fun createPortfolio(tokenToAdd: AddHotCryptoData): PortfolioSelectUM { - val accountIcon: AccountIconUM.CryptoPortfolio? + val accountIcon: AccountIconUM? val portfolioName: TextReference val isAccountMode = isAccountsModeEnabledUseCase.invokeSync() when (isAccountMode) { @@ -50,6 +50,7 @@ internal class OnrampAddTokenUiBuilder @Inject constructor( portfolioName = accountStatus.account.accountName.toUM().value accountIcon = when (accountStatus) { is AccountStatus.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon) + is AccountStatus.Payment -> AccountIconUM.Payment } } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt index 9489858928..8040f59244 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt @@ -23,7 +23,9 @@ internal class LoadingAccountTokenItemConverter( ).convert(TotalFiatBalance.Failed), isExpanded = true, isCollapsable = false, - tokens = currencies.flattenCurrencies().map(LoadingTokenListItemConverter::convert).toPersistentList(), + tokens = currencies.flattenCurrencies() + .map { LoadingTokenListItemConverter.convert(it.currency) } + .toPersistentList(), ) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt index 55e336791d..68932d0f66 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt @@ -3,6 +3,7 @@ package com.tangem.features.onramp.swap.availablepairs.entity.converters import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.converter.Converter @@ -11,12 +12,12 @@ import com.tangem.utils.converter.Converter * [REDACTED_AUTHOR] */ -internal object LoadingTokenListItemConverter : Converter { +internal object LoadingTokenListItemConverter : Converter { - override fun convert(value: CryptoCurrencyStatus): TokensListItemUM.Token { + override fun convert(value: CryptoCurrency): TokensListItemUM.Token { return TokensListItemUM.Token( state = TokenItemState.Loading( - id = value.currency.id.value, + id = value.id.value, iconState = CurrencyIconState.Loading, titleState = TokenItemState.TitleState.Loading, subtitleState = TokenItemState.SubtitleState.Loading, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt index ccf527232b..02b9d632dc 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt @@ -18,7 +18,9 @@ internal class SetLoadingTokenItemsTransformer( override fun transform(prevState: TokenListUM): TokenListUM { return prevState.copy( - availableItems = LoadingTokenListItemConverter.convertList(input = statuses).toImmutableList(), + availableItems = LoadingTokenListItemConverter.convertList( + input = statuses.map(CryptoCurrencyStatus::currency), + ).toImmutableList(), unavailableItems = persistentListOf(), warning = null, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index 84d7dc9a8f..5466084880 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -21,6 +21,7 @@ import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase 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.tokenlist.TokenList @@ -287,7 +288,7 @@ internal class AvailableSwapPairsModel @Inject constructor( ): TokenListUMTransformer { val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding - val filterByQueryAccountList = accountList + val filterByQueryAccountList: Map> = accountList .associate { accountStatus -> when (accountStatus) { is AccountStatus.CryptoPortfolio -> { @@ -300,6 +301,7 @@ internal class AvailableSwapPairsModel @Inject constructor( accountStatus.account to statuses } + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } } .filterValues { it.isNotEmpty() } @@ -360,7 +362,7 @@ internal class AvailableSwapPairsModel @Inject constructor( onRefresh = { modelScope.launch { if (networkInfo != null) { - accountList.filterIsInstance() + accountList.filterCryptoPortfolio() .forEach { (_, currencies) -> updateAvailablePairs(networkInfo, currencies.flattenCurrencies()) } @@ -384,7 +386,7 @@ internal class AvailableSwapPairsModel @Inject constructor( val accountList = accountListFlow.firstOrNull() ?: return@collectLatest updateAvailablePairs( networkInfo = networkInfo, - statuses = accountList.filterIsInstance() + statuses = accountList.filterCryptoPortfolio() .flatMap { accountStatus -> accountStatus.flattenCurrencies() }.toSet().toList(), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt index b3a75c9a89..a7ca52c790 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt @@ -2,6 +2,8 @@ package com.tangem.features.onramp.tokenlist.entity.transformer import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingAccountTokenItemConverter import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingTokenListItemConverter import com.tangem.features.onramp.tokenlist.entity.TokenListUM @@ -22,6 +24,7 @@ internal class SetLoadingAccountTokenListTransformer( val totalTokensCount = accountList.sumOf { account -> when (account) { is AccountStatus.CryptoPortfolio -> account.tokenList.flattenCurrencies().size + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } } @@ -31,7 +34,7 @@ internal class SetLoadingAccountTokenListTransformer( tokensListData = if (isAccountsMode) { TokenListUMData.AccountList( tokensList = accountListItemConverter.convertList( - accountList.filterIsInstance(), + accountList.filterCryptoPortfolio(), ).toPersistentList(), totalTokensCount = totalTokensCount, ) @@ -40,8 +43,9 @@ internal class SetLoadingAccountTokenListTransformer( tokensList = accountList.flatMap { account -> when (account) { is AccountStatus.CryptoPortfolio -> LoadingTokenListItemConverter.convertList( - account.tokenList.flattenCurrencies(), + account.tokenList.flattenCurrencies().map(CryptoCurrencyStatus::currency), ) + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } }.toPersistentList(), totalTokensCount = totalTokensCount, 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 e36eabad90..a90f9e6e25 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 @@ -295,13 +295,16 @@ internal class OnrampTokenListModel @Inject constructor( ) } - private fun AccountStatusList.filterAccountsByQuery(query: String) = accountStatuses.asSequence() + private fun AccountStatusList.filterAccountsByQuery( + query: String, + ): Map> = accountStatuses.asSequence() .associate { accountStatus -> when (accountStatus) { is AccountStatus.CryptoPortfolio -> { val filteredList = accountStatus.tokenList.flattenCurrencies().filterByQuery(query = query) accountStatus.account to filteredList } + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } }.filter { (_, value) -> value.isNotEmpty() } 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 c7f8ef28fe..cfbdeea741 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 @@ -6,6 +6,7 @@ import com.tangem.domain.account.producer.SingleAccountProducer import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.account.supplier.SingleAccountSupplier import com.tangem.domain.models.PortfolioId +import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId @@ -55,7 +56,10 @@ internal class ReferralInteractorImpl( ) ?: error("Account not found: ${portfolioId.accountId}") - account.derivationIndex + when (account) { + is Account.CryptoPortfolio -> account.derivationIndex + is Account.Payment -> TODO("[REDACTED_JIRA]") + } } is PortfolioId.Wallet -> null } diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt index 46ce302791..eddb1e949d 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt @@ -127,6 +127,7 @@ internal class ReferralModel @Inject constructor( val cryptoPortfolio = when (selectedAccount) { is AccountStatus.CryptoPortfolio -> selectedAccount + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } val awardCryptoCurrency = referralInteractor.getCryptoCurrency( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 2230676a18..83fb037866 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -21,6 +21,7 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.SendComponent diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt index 5626f5a93c..7ddfee6d8f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt @@ -22,14 +22,17 @@ internal class SendAnalyticHelper @Inject constructor( cryptoCurrency: CryptoCurrency, sendUM: SendUM, feeToken: CryptoCurrency, - account: Account.CryptoPortfolio?, + account: Account?, ) { val destinationUM = sendUM.destinationUM as? DestinationUM.Content val feeSelectorUM = sendUM.feeSelectorUM as? FeeSelectorUM.Content ?: return val feeType = feeSelectorUM.toAnalyticType() val feeTokenSymbol = feeToken.symbol - val isNotMainAccount = account != null && !account.isMainAccount - val derivationIndex = if (isNotMainAccount) account.derivationIndex.value else null + val derivationIndex = when (account) { + is Account.CryptoPortfolio -> if (!account.isMainAccount) account.derivationIndex else null + is Account.Payment -> null + null -> null + } analyticsEventHandler.send( SendAnalyticEvents.TransactionScreenOpened( token = cryptoCurrency.symbol, @@ -37,7 +40,7 @@ internal class SendAnalyticHelper @Inject constructor( blockchain = cryptoCurrency.network.name, isNonceNotEmpty = feeSelectorUM.feeNonce is FeeNonce.Nonce, ensStatus = getEnsStatus(sendUM), - derivationIndex = derivationIndex, + derivationIndex = derivationIndex?.value, feeToken = feeTokenSymbol, ), ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt index 423fc07b00..7bdbc5bb26 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt @@ -158,7 +158,7 @@ internal class SendConfirmComponent( val feeCryptoCurrencyStatus: CryptoCurrencyStatus, val cryptoCurrencyStatusFlow: StateFlow, val feeCryptoCurrencyStatusFlow: StateFlow, - val accountFlow: StateFlow, + val accountFlow: StateFlow, val isAccountModeFlow: StateFlow, val appCurrency: AppCurrency, val callback: ModelCallback, 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 2f0b7ffd83..c8eeee19df 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 @@ -147,7 +147,7 @@ internal class SendModel @Inject constructor( ), ) - val accountFlow: StateFlow + val accountFlow: StateFlow field = MutableStateFlow(null) val isAccountModeFlow: StateFlow field = MutableStateFlow(false) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt index d776414db6..73eac2a41a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt @@ -24,7 +24,7 @@ internal sealed class SendAmountComponentParams { abstract val cryptoCurrency: CryptoCurrency abstract val cryptoCurrencyStatusFlow: StateFlow abstract val isBalanceHidingFlow: StateFlow - abstract val accountFlow: StateFlow + abstract val accountFlow: StateFlow abstract val isAccountModeFlow: StateFlow data class AmountParams( @@ -37,7 +37,7 @@ internal sealed class SendAmountComponentParams { override val cryptoCurrencyStatusFlow: StateFlow, override val isBalanceHidingFlow: StateFlow, override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource, - override val accountFlow: StateFlow, + override val accountFlow: StateFlow, override val isAccountModeFlow: StateFlow, val callback: ModelCallback, val currentRoute: StateFlow, @@ -53,7 +53,7 @@ internal sealed class SendAmountComponentParams { override val cryptoCurrencyStatusFlow: StateFlow, override val isBalanceHidingFlow: StateFlow, override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource, - override val accountFlow: StateFlow, + override val accountFlow: StateFlow, override val isAccountModeFlow: StateFlow, val userWallet: UserWallet, val blockClickEnableFlow: StateFlow, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index bc73a06d62..21fdc47353 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -149,7 +149,7 @@ internal class SendAmountModel @Inject constructor( private suspend fun initMinBoundary( cryptoCurrencyStatus: CryptoCurrencyStatus, - account: Account.CryptoPortfolio?, + account: Account?, isAccountsMode: Boolean, ) { minAmountBoundary = getMinimumTransactionAmountSyncUseCase( @@ -178,11 +178,7 @@ internal class SendAmountModel @Inject constructor( } } - private fun initialState( - cryptoCurrencyStatus: CryptoCurrencyStatus, - account: Account.CryptoPortfolio?, - isAccountsMode: Boolean, - ) { + private fun initialState(cryptoCurrencyStatus: CryptoCurrencyStatus, account: Account?, isAccountsMode: Boolean) { val userWallet = userWallet if (uiState.value is AmountState.Empty && userWallet != null) { val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1 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 ba2ab8f1b1..95e67b5de3 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 @@ -14,7 +14,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase -import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.network.CryptoCurrencyAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isLocked @@ -272,36 +272,40 @@ internal class SendDestinationModel @Inject constructor( return combine( flow = getWalletsUseCase().conflate(), flow2 = multiAccountStatusListSupplier().conflate(), - ) { wallets, accountList -> + ) { wallets, accountStatusLists -> val cryptoCurrencyNetwork = cryptoCurrency.network coroutineScope { - accountList.mapNotNull { accountStatusList -> - val wallet = - wallets.filterNot { it.isLocked }.firstOrNull { it.walletId == accountStatusList.userWalletId } - ?: return@mapNotNull null + accountStatusLists.mapNotNull { accountStatusList -> + val wallet = wallets + .filterNot { it.isLocked } + .firstOrNull { it.walletId == accountStatusList.userWalletId } + ?: return@mapNotNull null async { - accountStatusList.accountStatuses.map { accountStatus -> - async { - accountStatus.flattenCurrencies() - .filter { it.currency.network.rawId == cryptoCurrencyNetwork.rawId } - .mapNotNull { cryptoCurrencyStatus -> - val address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value - ?: return@mapNotNull null + accountStatusList.flattenCurrencies() + .filter { it.currency.network.rawId == cryptoCurrencyNetwork.rawId } + .mapNotNull { cryptoCurrencyStatus -> + val address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: return@mapNotNull null - async { - DestinationWalletUM( - name = wallet.name, - address = address, - cryptoCurrency = cryptoCurrencyStatus.currency, - userWalletId = wallet.walletId, - account = accountStatus.account as? Account.CryptoPortfolio, - ) - } - }.awaitAll() + // 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, + ) } - }.awaitAll().flatten() } }.awaitAll().flatten() } 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 c70c805b16..5b47c7de51 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 @@ -1,9 +1,11 @@ package com.tangem.features.send.v2.subcomponents.destination.model.converters +import com.tangem.common.ui.account.AccountIconUM import com.tangem.common.ui.account.AccountTitleUM import com.tangem.common.ui.account.CryptoPortfolioIconConverter import com.tangem.common.ui.account.toUM import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationRecipientListUM import com.tangem.features.send.v2.subcomponents.destination.model.transformers.WALLET_DEFAULT_COUNT @@ -59,7 +61,12 @@ internal class SendRecipientWalletListConverter( accountTitleUM = if (account != null && isAccountsMode) { AccountTitleUM.Account( name = account.accountName.toUM().value, - icon = CryptoPortfolioIconConverter.convert(account.icon), + icon = when (account) { + is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert( + account.icon, + ) + is Account.Payment -> AccountIconUM.Payment + }, prefixText = stringReference(StringsSigns.DOT), ) } else { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationWalletUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationWalletUM.kt index f9f611393b..7cdbd207a8 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationWalletUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationWalletUM.kt @@ -20,5 +20,5 @@ data class DestinationWalletUM( val userWalletId: UserWalletId, val address: String, val cryptoCurrency: CryptoCurrency, - val account: Account.CryptoPortfolio? = null, + val account: Account? = null, ) \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt index a2f39e1550..bee0732784 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt @@ -24,7 +24,7 @@ internal sealed class SwapAmountComponentParams { abstract val primaryCryptoCurrencyStatusFlow: StateFlow abstract val secondaryCryptoCurrency: CryptoCurrency? abstract val filterProviderTypes: List - abstract val accountFlow: StateFlow + abstract val accountFlow: StateFlow abstract val isAccountModeFlow: StateFlow data class AmountParams( @@ -37,7 +37,7 @@ internal sealed class SwapAmountComponentParams { override val secondaryCryptoCurrency: CryptoCurrency?, override val filterProviderTypes: List = emptyList(), override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource, - override val accountFlow: StateFlow, + override val accountFlow: StateFlow, override val isAccountModeFlow: StateFlow, val title: TextReference, val callback: SwapAmountComponent.ModelCallback, @@ -54,7 +54,7 @@ internal sealed class SwapAmountComponentParams { override val secondaryCryptoCurrency: CryptoCurrency?, override val filterProviderTypes: List = emptyList(), override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource, - override val accountFlow: StateFlow, + override val accountFlow: StateFlow, override val isAccountModeFlow: StateFlow, val blockClickEnableFlow: StateFlow, ) : SwapAmountComponentParams() 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 9dd4198d6b..ae17646047 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 @@ -30,7 +30,7 @@ internal class SwapAmountFieldConverter( private val clickIntents: AmountScreenClickIntents, private val isSingleWallet: Boolean, private val isAccountsMode: Boolean, - private val account: Account.CryptoPortfolio?, + private val account: Account?, ) { private val iconStateConverter = CryptoCurrencyToIconStateConverter() 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 3febeaf504..56a7fab934 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 @@ -21,7 +21,7 @@ internal class SwapAmountBalanceHiddenTransformer( private val swapDirection: SwapDirection, private val clickIntents: AmountScreenClickIntents, private val isAccountsMode: Boolean, - private val account: Account.CryptoPortfolio?, + private val account: Account?, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { 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 ca25cc3fda..aa09d11164 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 @@ -27,7 +27,7 @@ internal class SwapAmountPrimaryReadyStateTransformer( private val isShowBestRateAnimation: Boolean, private val isSingleWallet: Boolean, private val isAccountsMode: Boolean, - private val account: Account.CryptoPortfolio?, + private val account: Account?, ) : Transformer { private val amountFieldConverter = SwapAmountFieldConverter( 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 298b11ad75..f8f8600421 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 @@ -28,7 +28,7 @@ internal class SwapAmountSecondaryReadyStateTransformer( private val isShowBestRateAnimation: Boolean, private val isSingleWallet: Boolean, private val isAccountsMode: Boolean, - private val account: Account.CryptoPortfolio?, + private val account: Account?, ) : Transformer { private val amountFieldConverter = SwapAmountFieldConverter( 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 b825e3f9e7..e0fd2912f4 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 @@ -18,7 +18,7 @@ internal data class ConfirmData( val feeError: GetFeeError?, val fromCryptoCurrencyStatus: CryptoCurrencyStatus?, val toCryptoCurrencyStatus: CryptoCurrencyStatus?, - val fromAccount: Account.CryptoPortfolio?, + val fromAccount: Account?, val quote: SwapQuoteUM?, val rateType: ExpressRateType?, ) \ No newline at end of file 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 300dd949eb..b6d57e6d09 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,6 +20,7 @@ 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 @@ -110,8 +111,11 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( } is SendWithSwapConfirmComponent -> { val fromCurrency = params.currency - val fromDerivationIndex = model.accountFlow.value?.derivationIndex?.value - .takeIf { model.isAccountModeFlow.value } + 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, 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 9bae584628..aac3bb92dd 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 @@ -181,7 +181,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( val isBalanceHidingFlow: StateFlow, val primaryCryptoCurrencyStatusFlow: StateFlow, val primaryFeePaidCurrencyStatusFlow: StateFlow, // doesn't change if select gasless fee - val accountFlow: StateFlow, + val accountFlow: StateFlow, val isAccountModeFlow: StateFlow, val callback: ModelCallback, ) 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 4c22e6b47c..9de68cfb9a 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 @@ -24,6 +24,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.express.models.ExpressProviderType +import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.isHotWallet 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 7a166639ff..3618110f82 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 @@ -129,7 +129,7 @@ internal class SwapTransactionSender @AssistedInject constructor( fromAmount: BigDecimal, fromStatus: CryptoCurrencyStatus, toStatus: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, fee: Fee, feeExtended: TransactionFeeExtended?, provider: ExpressProvider, 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 629d37898d..31b4ba8651 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 @@ -96,7 +96,7 @@ internal class SendWithSwapModel @Inject constructor( ), ) - val accountFlow: StateFlow + val accountFlow: StateFlow field = MutableStateFlow(null) val isAccountModeFlow: StateFlow diff --git a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt index d2ef17de45..ed9144cac3 100644 --- a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt +++ b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt @@ -18,7 +18,7 @@ interface SwapComponent : ComposableContentComponent { val screenSource: String, val tangemPayInput: TangemPayInput? = null, val preselectedToToken: CryptoCurrencyStatus? = null, - val preselectedAccount: Account.CryptoPortfolio? = null, + val preselectedAccount: Account? = null, ) { data class TangemPayInput( val cryptoAmount: BigDecimal, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt index fb7b9d0ed1..a04749322a 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt @@ -47,8 +47,8 @@ internal class DefaultSwapTransactionRepository( userWalletId: UserWalletId, fromCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, transaction: SavedSwapTransactionModel, ) { transaction.status?.let { status -> @@ -248,8 +248,8 @@ internal class DefaultSwapTransactionRepository( userWalletId: UserWalletId, fromCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, transactions: List, ): List { return addOrReplace( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt index 5c0b55b780..2b50f90d14 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt @@ -120,8 +120,8 @@ internal class SavedSwapTransactionListConverter( userWalletId: UserWalletId, fromCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, tokenTransactions: List, ) = SavedSwapTransactionListModelInner( userWalletId = userWalletId.stringValue, @@ -161,10 +161,7 @@ internal class SavedSwapTransactionListConverter( } } - private fun findAccountByDerivationIndex( - accountList: AccountList?, - derivationIndex: DerivationIndex?, - ): Account.CryptoPortfolio? { + private fun findAccountByDerivationIndex(accountList: AccountList?, derivationIndex: DerivationIndex?): Account? { return accountList?.accounts?.asSequence()?.filterIsInstance() ?.firstOrNull { it.derivationIndex == derivationIndex } } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 1c31e6c77a..244a784b00 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -80,8 +80,8 @@ interface SwapInteractor { swapData: SwapDataModel?, currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, amountToSwap: String, includeFeeInAmount: IncludeFeeInAmount, fee: TxFee?, @@ -129,8 +129,8 @@ interface SwapInteractor { suspend fun storeSwapTransaction( currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, amount: SwapAmount, swapProvider: SwapProvider, swapDataModel: SwapDataModel, @@ -154,9 +154,9 @@ interface SwapInteractor { @Suppress("LongParameterList") suspend fun loadFeeForSwapTransaction( fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, amount: String, reduceBalanceBy: BigDecimal, provider: SwapProvider, @@ -176,9 +176,9 @@ interface SwapInteractor { @Suppress("LongParameterList") suspend fun loadFeeForSwapTransaction( fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, amount: String, reduceBalanceBy: BigDecimal, provider: SwapProvider, 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 046dc76c7f..060390b7a6 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 @@ -29,6 +29,7 @@ import com.tangem.domain.demo.IsDemoCardUseCase 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.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network @@ -180,17 +181,11 @@ internal class SwapInteractorImpl @AssistedInject constructor( SingleAccountStatusListProducer.Params(userWalletId), )?.accountStatuses.orEmpty() - val walletAccountCurrencyStatusesExceptInitial = walletAccountCurrencyStatuses - .mapNotNull { accountStatus -> - val filteredCurrencies = accountStatus.flattenCurrencies().filter { status -> - val isDifferentCurrency = status.currency.network.backendId != currency.network.backendId || - status.currency.getContractAddress() != currency.getContractAddress() - - val hasValidStatus = - status.value is CryptoCurrencyStatus.Loaded || status.value is CryptoCurrencyStatus.NoAccount - val isNotCustomToken = !status.currency.isCustom - - hasValidStatus && isDifferentCurrency && isNotCustomToken + val walletAccountCurrencyStatusesExceptInitial: Map> = + walletAccountCurrencyStatuses.mapNotNull { accountStatus -> + val filteredCurrencies = when (accountStatus) { + is AccountStatus.CryptoPortfolio -> accountStatus.flattenCurrencies().filterCurrencies(currency) + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } if (filteredCurrencies.isNotEmpty()) { @@ -234,6 +229,17 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } + private fun List.filterCurrencies(currency: CryptoCurrency) = this.filter { status -> + val isDifferentCurrency = status.currency.network.backendId != currency.network.backendId || + status.currency.getContractAddress() != currency.getContractAddress() + + val hasValidStatus = + status.value is CryptoCurrencyStatus.Loaded || status.value is CryptoCurrencyStatus.NoAccount + val isNotCustomToken = !status.currency.isCustom + + hasValidStatus && isDifferentCurrency && isNotCustomToken + } + private suspend fun getToCurrenciesGroup( currency: CryptoCurrency, leastPairs: List, @@ -293,7 +299,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val isUnavailable = providers.isNullOrEmpty() AccountSwapCurrency( isAvailable = !isUnavailable, - account = cryptoPortfolio, + account = accountEntry, cryptoCurrencyStatus = currencyStatus, providers = providers.orEmpty(), ) @@ -400,7 +406,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( """ Find the best quote |- fromToken: $fromToken + |- fromAccount: $fromAccount |- toToken: $toToken + |- toAccount: $toAccount |- providers: $providers |- amountToSwap: $amountToSwap |- selectedFee: $txFeeSealedState @@ -767,8 +775,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapData: SwapDataModel?, currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, amountToSwap: String, includeFeeInAmount: IncludeFeeInAmount, fee: TxFee?, @@ -848,8 +856,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapData: SwapDataModel, currencyToSendStatus: CryptoCurrencyStatus, currencyToGetStatus: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, amountToSwap: String, txFee: TxFee, ): SwapTransactionState { @@ -890,8 +898,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapData: SwapDataModel, currencyToSendStatus: CryptoCurrencyStatus, currencyToGetStatus: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, amountToSwap: String, ): SwapTransactionState { val dexTransaction = swapData.transaction as? ExpressTransactionModel.DEX @@ -919,8 +927,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapData: SwapDataModel, currencyToSendStatus: CryptoCurrencyStatus, currencyToGetStatus: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, amount: SwapAmount, txData: TransactionData, payInAddress: String, @@ -988,8 +996,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun onSwapCex( currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, amount: SwapAmount, txFee: TxFee?, swapProvider: SwapProvider, @@ -1150,8 +1158,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( override suspend fun storeSwapTransaction( currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, amount: SwapAmount, swapProvider: SwapProvider, swapDataModel: SwapDataModel, @@ -1187,9 +1195,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( @Suppress("LongParameterList") override suspend fun loadFeeForSwapTransaction( fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, amount: String, reduceBalanceBy: BigDecimal, provider: SwapProvider, @@ -1225,9 +1233,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( override suspend fun loadFeeForSwapTransaction( fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, amount: String, reduceBalanceBy: BigDecimal, provider: SwapProvider, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt index 122d227590..0720aa7abf 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt @@ -17,8 +17,8 @@ interface SwapTransactionRepository { userWalletId: UserWalletId, fromCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, transaction: SavedSwapTransactionModel, ) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt index f25e4b1c1a..31236c6313 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt @@ -13,8 +13,8 @@ data class SavedSwapTransactionListModel( val toCryptoCurrencyId: String, val fromCryptoCurrency: CryptoCurrency, val toCryptoCurrency: CryptoCurrency, - val fromAccount: Account.CryptoPortfolio?, - val toAccount: Account.CryptoPortfolio?, + val fromAccount: Account?, + val toAccount: Account?, val transactions: List, ) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt index 7ad787a146..4619adf9ab 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt @@ -36,8 +36,8 @@ sealed class SwapTransactionState { data class StoreTransactionData( val currencyToSend: CryptoCurrencyStatus, val currencyToGet: CryptoCurrencyStatus, - val fromAccount: Account.CryptoPortfolio?, - val toAccount: Account.CryptoPortfolio?, + val fromAccount: Account?, + val toAccount: Account?, val amount: SwapAmount, val swapProvider: SwapProvider, val swapDataModel: SwapDataModel, diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt index aa4c1a4b46..6c53807de1 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt @@ -22,6 +22,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase 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.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents @@ -84,11 +85,11 @@ internal class AccountItemsDelegate @Inject constructor( return WalletSettingsAccountsUM.Account(state = accountItemUM) } - fun mapAccount(account: AccountStatus): WalletSettingsAccountsUM = when (account) { + fun mapAccount(account: AccountStatus.CryptoPortfolio): WalletSettingsAccountsUM = when (account) { is AccountStatus.CryptoPortfolio -> account.mapCryptoPortfolio() } - val accounts = accountStatusList.accountStatuses + val accounts = accountStatusList.accountStatuses.filterCryptoPortfolio() val header = WalletSettingsAccountsUM.Header( id = "accounts_header", diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt index d62385f7be..770ada3985 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt @@ -2,7 +2,7 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.model.AccountCryptoCurrencies -import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.tokenlist.TokenList import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem @@ -34,6 +34,7 @@ internal class CryptoCurrenciesIdsResolver { } } + @Suppress("UseOrEmpty") fun resolveV2(tokensListUM: OrganizeTokensListUM, accountStatusList: AccountStatusList?): AccountCryptoCurrencies { val draggableTokens = when (tokensListUM) { OrganizeTokensListUM.EmptyList -> return emptyMap() @@ -43,10 +44,11 @@ internal class CryptoCurrenciesIdsResolver { } return accountStatusList?.accountStatuses - ?.filter { it.getCryptoTokenList() != TokenList.Empty } + ?.filterCryptoPortfolio() + ?.filter { it.tokenList != TokenList.Empty } ?.associate { accountStatus -> val currencies = accountStatus.flattenCurrencies() - accountStatus.account as Account.CryptoPortfolio to draggableTokens + accountStatus.account to draggableTokens .asSequence() .filter { it.accountId == accountStatus.account.accountId.value } .mapNotNull { sortedToken -> diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt index 75e1c92282..279d59f8ed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt @@ -6,7 +6,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.tokenlist.TokenList import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM @@ -61,7 +61,7 @@ internal class AccountTokenItemConverter( isGrouped = isGrouping, items = value.accountStatuses .asSequence() - .filterIsInstance() + .filterCryptoPortfolio() .flatMap { accountStatus -> if (accountStatus.tokenList != TokenList.Empty) { buildList { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt index 295848f5f4..c0938d2748 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt @@ -2,7 +2,6 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.it import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus 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.tokenlist.TokenList import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem @@ -13,27 +12,26 @@ import kotlinx.collections.immutable.toPersistentList internal class OrganizedTokenListConverter( private val appCurrency: AppCurrency, -) : Converter> { +) : Converter> { private val tokensConverter by lazy { CryptoCurrencyToDraggableItemConverterV2(appCurrency) } private val groupsConverter by lazy { NetworkGroupToDraggableItemsConverterV2(tokensConverter) } - override fun convert(value: AccountStatus): PersistentList { - val cryptoAccount = value.account as? Account.CryptoPortfolio ?: return persistentListOf() - return when (val tokenList = value.getCryptoTokenList()) { + override fun convert(value: AccountStatus.CryptoPortfolio): PersistentList { + return when (val tokenList = value.tokenList) { is TokenList.GroupedByNetwork -> groupsConverter.convertList( - tokenList.groups.map { cryptoAccount to it }, + tokenList.groups.map { value.account to it }, ) .flatten() .toPersistentList() is TokenList.Ungrouped -> tokensConverter.convertList( - value.flattenCurrencies().map { + value.flattenCurrencies().map { cryptoCurrencyStatus -> AccountCryptoCurrencyStatus( - account = cryptoAccount, - status = it, + account = value.account, + status = cryptoCurrencyStatus, ) }, ).toPersistentList() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt index 38197dbb07..c7fe144bd9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt @@ -241,10 +241,11 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( } } - private fun getAccountStatusFlow(userWallet: UserWallet): Flow { + private fun getAccountStatusFlow(userWallet: UserWallet): Flow { val accountId = AccountId.forMainCryptoPortfolio(userWalletId = userWallet.walletId) return singleAccountStatusSupplier(SingleAccountStatusProducer.Params(accountId)) + .filterIsInstance() .distinctUntilChanged() .conflate() } 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 1a6b19c186..1847ff842f 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 @@ -14,6 +14,7 @@ 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.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.tokenlist.TokenList @@ -144,6 +145,7 @@ internal class TokenListStateConverter( } val accountItems = accountList.accountStatuses + .filterCryptoPortfolio() .map { accountStatus -> when (accountStatus) { is AccountStatus.CryptoPortfolio -> accountStatus.map() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicWalletSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicWalletSubscriber.kt index 585a48c0b0..b169aba32c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicWalletSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicWalletSubscriber.kt @@ -4,6 +4,7 @@ import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import kotlinx.coroutines.flow.Flow @@ -45,11 +46,9 @@ internal abstract class BasicWalletSubscriber : WalletSubscriber() { protected fun getMainAccountStatusFlow(): Flow { return getAccountStatusListFlow() .mapNotNull { accountStatusList -> - accountStatusList.accountStatuses.find { accountStatus -> - when (accountStatus) { - is AccountStatus.CryptoPortfolio -> accountStatus.account.isMainAccount - } - } as? AccountStatus.CryptoPortfolio + accountStatusList.accountStatuses + .filterCryptoPortfolio() + .find { accountStatus -> accountStatus.account.isMainAccount } } .distinctUntilChanged() .conflate() diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index f9fefde39c..d686883940 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -27,7 +27,6 @@ import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus -import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -105,7 +104,7 @@ internal class WcPairModel @Inject constructor( private val selectedUserWalletFlow: MutableStateFlow by lazy { MutableStateFlow(getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }) } - private val selectedPortfolio = MutableSharedFlow>( + private val selectedPortfolio = MutableSharedFlow>( replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) @@ -171,7 +170,7 @@ internal class WcPairModel @Inject constructor( private suspend fun handlePairState( pairState: WcPairState, portfolios: PortfolioFetcher.Data? = null, - selected: Pair? = null, + selected: Pair? = null, isAccountMode: Boolean? = null, ) { when (pairState) { @@ -206,7 +205,7 @@ internal class WcPairModel @Inject constructor( private suspend fun handleProposalState( pairState: WcPairState.Proposal, portfolios: PortfolioFetcher.Data? = null, - selected: Pair? = null, + selected: Pair? = null, ) { val availableWallets = pairState.dAppSession.proposalNetwork.keys .filter { !it.isLocked && it.isMultiCurrency } @@ -266,7 +265,7 @@ internal class WcPairModel @Inject constructor( } private suspend fun tryToCreatePortfolioSelectRow( - selectedPortfolio: Pair?, + selectedPortfolio: Pair?, portfolios: PortfolioFetcher.Data?, ): PortfolioSelectUM? { selectedPortfolio ?: return null @@ -274,7 +273,6 @@ internal class WcPairModel @Inject constructor( val (wallet, portfolioAccount) = selectedPortfolio val account = when (val account = portfolioAccount.account) { is Account.CryptoPortfolio -> account - is Account.Payment -> TODO("[REDACTED_JIRA]") } val isAccountMode = selectorController.isAccountMode.first() val icon: AccountIconUM.CryptoPortfolio? diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsAccountModeTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsAccountModeTransformer.kt index fa672da841..f3991ee130 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsAccountModeTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsAccountModeTransformer.kt @@ -48,7 +48,7 @@ internal class WcSessionsAccountModeTransformer( ) items.add(walletHeader) - accountList.accounts.forEach accountsForEach@{ account -> + accountList.accounts.filterIsInstance().forEach accountsForEach@{ account -> val accountSessions = sessions.filter { it.account?.accountId == account.accountId } if (accountSessions.isEmpty()) return@accountsForEach val connectedApps = accountSessions.map { dappSession -> @@ -65,7 +65,6 @@ internal class WcSessionsAccountModeTransformer( } val accountIcon = when (account) { is Account.CryptoPortfolio -> account.icon - is Account.Payment -> TODO("[REDACTED_JIRA]") } val accountTitle = AccountTitleUM.Account( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcPortfolioNameDelegate.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcPortfolioNameDelegate.kt index 5df48f4e4d..cad34dfe8e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcPortfolioNameDelegate.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcPortfolioNameDelegate.kt @@ -19,13 +19,12 @@ internal class WcPortfolioNameDelegate @AssistedInject constructor( isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, @Assisted private val scope: CoroutineScope, ) { - val isAccountMode = isAccountsModeEnabledUseCase.invoke() + private val isAccountMode = isAccountsModeEnabledUseCase.invoke() .stateIn(scope = scope, started = SharingStarted.Eagerly, initialValue = false) fun createAccountTitleUM(value: WcSession): AccountTitleUM? { val account = when (val account = value.account) { is Account.CryptoPortfolio -> account - is Account.Payment -> TODO("[REDACTED_JIRA]") null -> null } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt index 7f74dc7dca..11fba2839f 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt @@ -11,7 +11,6 @@ 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.ui.clipboard.ClipboardManager -import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.WcRequestUseCaseFactory import com.tangem.domain.walletconnect.usecase.method.WcAddNetworkUseCase diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 9eaceb4978..a156c5a69c 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -23,7 +23,6 @@ import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.core.lce.Lce -import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt index 1e97be37f4..7a0a21fe7f 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt @@ -12,7 +12,6 @@ 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.ui.clipboard.ClipboardManager -import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.WcRequestUseCaseFactory import com.tangem.domain.walletconnect.model.WcEthMethod From 4aa267c9517ed14a5513e5cb7d2dd826ec3c4433 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Feb 2026 21:44:35 +0500 Subject: [PATCH 040/106] Updated on 2026-08-14 --- .../feed/model/news/list/NewsListModel.kt | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/NewsListModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/NewsListModel.kt index 9e6153728b..22ba4a7be4 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/NewsListModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/NewsListModel.kt @@ -127,19 +127,17 @@ internal class NewsListModel @Inject constructor( } private fun onCategoryClick(categoryId: Int) { - val newCategoryId = if (state.value.selectedCategoryId == categoryId) { - DEFAULT_ALL_NEWS_CATEGORIES_ID - } else { - categoryId + if (state.value.selectedCategoryId == categoryId) { + return } - if (newCategoryId != DEFAULT_ALL_NEWS_CATEGORIES_ID) { - analyticsEventHandler.send(NewsListAnalyticsEvent.NewsCategoriesClick(newCategoryId)) + if (categoryId != DEFAULT_ALL_NEWS_CATEGORIES_ID) { + analyticsEventHandler.send(NewsListAnalyticsEvent.NewsCategoriesClick(categoryId)) } - selectedCategoryId.value = newCategoryId + selectedCategoryId.value = categoryId _state.update { currentState -> currentState.copy( - selectedCategoryId = newCategoryId, - filters = updateFilterChips(newCategoryId), + selectedCategoryId = categoryId, + filters = updateFilterChips(categoryId), ) } batchFlowManager.reload() From 05485710894b3430290f56883972ff6613e56cc1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 08:05:42 +0100 Subject: [PATCH 041/106] Updated on 2026-08-14 --- .../tangem/tap/di/domain/EarnDomainModule.kt | 10 + .../data/earn/datastore/EarnFilterStore.kt | 17 ++ .../data/earn/datastore/EarnNetworksStore.kt | 2 +- .../data/earn/datastore/EarnTopTokensStore.kt | 2 +- .../com/tangem/data/earn/di/EarnDataModule.kt | 3 + .../earn/repository/DefaultEarnRepository.kt | 11 + .../tangem/domain/earn/model/EarnFilter.kt | 9 + .../domain/earn/model/EarnFilterNetwork.kt | 27 ++ .../domain/earn/model/EarnFilterType.kt | 7 + .../domain/earn/repository/EarnRepository.kt | 5 + .../earn/usecase/GetEarnFilterUseCase.kt | 12 + .../earn/usecase/SetEarnFilterUseCase.kt | 11 + .../components/earn/DefaultEarnComponent.kt | 16 +- .../earn/EarnNetworkFilterComponent.kt | 43 +++ .../earn/EarnTypeFilterComponent.kt | 44 +++ .../components/feed/DefaultFeedComponent.kt | 12 +- .../components/feed/FeedBottomSheetRoute.kt | 14 + .../components/feed/FeedPortfolioRoute.kt | 11 - .../tangem/features/feed/di/ModelModule.kt | 12 + .../features/feed/model/earn/EarnModel.kt | 263 ++++++++---------- .../earn/filters/EarnNetworkFilterModel.kt | 37 +++ .../model/earn/filters/EarnTypeFilterModel.kt | 36 +++ .../state/EarnFilterNetworkConverter.kt | 31 +++ .../state/EarnFilterNetworkUMConverter.kt | 21 ++ .../filters/state/EarnFilterTypeConverter.kt | 15 + .../state/EarnFilterTypeUMConverter.kt | 16 ++ .../model/earn/state/EarnStateController.kt | 10 +- .../state/converter/EarnNetworksConverter.kt | 22 -- .../ClearEarnUMFilterStateTransformer.kt | 28 -- .../EarnFilterSelectedStateTransformer.kt | 18 ++ ...rnNetworkFilterSelectedStateTransformer.kt | 7 +- .../EarnNetworkFilterStateTransformer.kt | 21 -- .../EarnTypeFilterSelectedStateTransformer.kt | 18 -- .../EarnTypeFilterStateTransformer.kt | 21 -- .../UpdateEarnUMInitialStateTransformer.kt | 5 - .../feed/model/feed/FeedComponentModel.kt | 6 +- .../features/feed/ui/earn/EarnContent.kt | 21 +- .../EarnFilterByNetworkBottomSheet.kt | 37 +-- ...EarnFilterByNetworkBottomSheetContentUM.kt | 1 - .../feed/ui/earn/state/EarnFilterNetworkUM.kt | 12 +- .../features/feed/ui/earn/state/EarnUM.kt | 6 - 41 files changed, 573 insertions(+), 347 deletions(-) create mode 100644 data/earn/src/main/java/com/tangem/data/earn/datastore/EarnFilterStore.kt create mode 100644 domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilter.kt create mode 100644 domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilterNetwork.kt create mode 100644 domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilterType.kt create mode 100644 domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnFilterUseCase.kt create mode 100644 domain/earn/src/main/java/com/tangem/domain/earn/usecase/SetEarnFilterUseCase.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/EarnNetworkFilterComponent.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/EarnTypeFilterComponent.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedPortfolioRoute.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnNetworkFilterModel.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnTypeFilterModel.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterNetworkConverter.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterNetworkUMConverter.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterTypeConverter.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterTypeUMConverter.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/converter/EarnNetworksConverter.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/ClearEarnUMFilterStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterStateTransformer.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterSelectedStateTransformer.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterStateTransformer.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt index 55a22513b5..3ae0f6d5c6 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt @@ -45,4 +45,14 @@ object EarnDomainModule { fun provideGetEarnTokensBatchFlowUseCase(repository: EarnRepository): GetEarnTokensBatchFlowUseCase { return GetEarnTokensBatchFlowUseCase(repository) } + + @Provides + fun provideGetEarnFilterUseCase(repository: EarnRepository): GetEarnFilterUseCase { + return GetEarnFilterUseCase(repository) + } + + @Provides + fun provideSetEarnFilterUseCase(repository: EarnRepository): SetEarnFilterUseCase { + return SetEarnFilterUseCase(repository) + } } \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnFilterStore.kt b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnFilterStore.kt new file mode 100644 index 0000000000..3edbae153d --- /dev/null +++ b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnFilterStore.kt @@ -0,0 +1,17 @@ +package com.tangem.data.earn.datastore + +import com.tangem.datasource.local.datastore.RuntimeStateStore +import com.tangem.domain.earn.model.EarnFilter +import com.tangem.domain.earn.model.EarnFilterNetwork +import com.tangem.domain.earn.model.EarnFilterType +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class EarnFilterStore @Inject constructor() : + RuntimeStateStore by RuntimeStateStore( + defaultValue = EarnFilter( + earnFilterNetwork = EarnFilterNetwork.AllNetworks(isSelected = true), + earnFilterType = EarnFilterType.ALL, + ), + ) \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt index 6bde739432..c623e841eb 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt @@ -6,7 +6,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class EarnNetworksStore @Inject constructor() : +internal class EarnNetworksStore @Inject constructor() : RuntimeStateStore by RuntimeStateStore( defaultValue = null, ) \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt index 29f0cbc12e..73e05f6289 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt @@ -6,7 +6,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class EarnTopTokensStore @Inject constructor() : +internal class EarnTopTokensStore @Inject constructor() : RuntimeStateStore by RuntimeStateStore( defaultValue = null, ) \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt b/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt index 19674a4f95..45e45e897d 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt @@ -2,6 +2,7 @@ package com.tangem.data.earn.di import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.earn.DefaultEarnErrorResolver +import com.tangem.data.earn.datastore.EarnFilterStore import com.tangem.data.earn.datastore.EarnNetworksStore import com.tangem.data.earn.datastore.EarnTopTokensStore import com.tangem.data.earn.repository.DefaultEarnRepository @@ -31,6 +32,7 @@ internal object EarnDataModule { userWalletsListRepository: UserWalletsListRepository, earnNetworksStore: EarnNetworksStore, earnTopTokensStore: EarnTopTokensStore, + earnFilterStore: EarnFilterStore, earnErrorResolver: EarnErrorResolver, excludedBlockchains: ExcludedBlockchains, ): EarnRepository { @@ -42,6 +44,7 @@ internal object EarnDataModule { earnTopTokensStore = earnTopTokensStore, earnErrorResolver = earnErrorResolver, excludedBlockchains = excludedBlockchains, + earnFilterStore = earnFilterStore, ) } } \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt b/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt index 53c36696d4..95deebdb54 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt @@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.earn.converter.EarnTokenConverter +import com.tangem.data.earn.datastore.EarnFilterStore import com.tangem.data.earn.datastore.EarnNetworksStore import com.tangem.data.earn.datastore.EarnTopTokensStore import com.tangem.data.earn.repository.batch.EarnTokensBatchFetcher @@ -14,6 +15,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.EarnResponse import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.earn.EarnErrorResolver +import com.tangem.domain.earn.model.EarnFilter import com.tangem.domain.earn.model.EarnTokensBatchFlow import com.tangem.domain.earn.model.EarnTokensBatchingContext import com.tangem.domain.earn.repository.EarnRepository @@ -38,6 +40,7 @@ internal class DefaultEarnRepository( private val userWalletsListRepository: UserWalletsListRepository, private val earnNetworksStore: EarnNetworksStore, private val earnTopTokensStore: EarnTopTokensStore, + private val earnFilterStore: EarnFilterStore, private val earnErrorResolver: EarnErrorResolver, private val excludedBlockchains: ExcludedBlockchains, ) : EarnRepository { @@ -123,6 +126,14 @@ internal class DefaultEarnRepository( return earnTopTokensStore.get() } + override fun observeEarnFilter(): Flow { + return earnFilterStore.get() + } + + override suspend fun setEarnFilter(filter: EarnFilter) { + earnFilterStore.store(filter) + } + companion object { internal const val FIRST_PAGE = 1 private const val INITIAL_BATCH_KEY = 0 diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilter.kt b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilter.kt new file mode 100644 index 0000000000..e09972f216 --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilter.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.earn.model + +import kotlinx.serialization.Serializable + +@Serializable +data class EarnFilter( + val earnFilterNetwork: EarnFilterNetwork, + val earnFilterType: EarnFilterType, +) \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilterNetwork.kt b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilterNetwork.kt new file mode 100644 index 0000000000..12de9b30ba --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilterNetwork.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.earn.model + +import kotlinx.serialization.Serializable + +@Serializable +sealed interface EarnFilterNetwork { + + val isSelected: Boolean + + @Serializable + data class AllNetworks( + override val isSelected: Boolean, + ) : EarnFilterNetwork + + @Serializable + data class MyNetworks( + override val isSelected: Boolean, + ) : EarnFilterNetwork + + @Serializable + data class Specific( + override val isSelected: Boolean, + val id: String, + val symbol: String, + val fullName: String, + ) : EarnFilterNetwork +} \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilterType.kt b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilterType.kt new file mode 100644 index 0000000000..26c8760325 --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilterType.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.earn.model + +enum class EarnFilterType { + ALL, + STAKING, + YIELD, +} \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt b/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt index e20bab51ec..7286e647c4 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt @@ -1,5 +1,6 @@ package com.tangem.domain.earn.repository +import com.tangem.domain.earn.model.EarnFilter import com.tangem.domain.earn.model.EarnTokensBatchFlow import com.tangem.domain.earn.model.EarnTokensBatchingContext import com.tangem.domain.models.earn.EarnNetworks @@ -23,4 +24,8 @@ interface EarnRepository { suspend fun fetchTopEarnTokens(limit: Int) fun observeTopEarnTokens(): Flow + + fun observeEarnFilter(): Flow + + suspend fun setEarnFilter(filter: EarnFilter) } \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnFilterUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnFilterUseCase.kt new file mode 100644 index 0000000000..0719f4b643 --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnFilterUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.earn.usecase + +import com.tangem.domain.earn.model.EarnFilter +import com.tangem.domain.earn.repository.EarnRepository +import kotlinx.coroutines.flow.Flow + +class GetEarnFilterUseCase(private val repository: EarnRepository) { + + operator fun invoke(): Flow { + return repository.observeEarnFilter() + } +} \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/SetEarnFilterUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/SetEarnFilterUseCase.kt new file mode 100644 index 0000000000..3f666e4508 --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/SetEarnFilterUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.earn.usecase + +import com.tangem.domain.earn.model.EarnFilter +import com.tangem.domain.earn.repository.EarnRepository + +class SetEarnFilterUseCase(private val repository: EarnRepository) { + + suspend operator fun invoke(filter: EarnFilter) { + repository.setEarnFilter(filter) + } +} \ 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 3da82e53fe..679bc440fa 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 @@ -19,7 +19,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.LocalMainBottomSheetColor -import com.tangem.features.feed.components.feed.FeedPortfolioRoute +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.earn.EarnContent @@ -35,7 +35,7 @@ internal class DefaultEarnComponent( private val bottomSheetSlot = childSlot( source = earnModel.bottomSheetNavigation, - serializer = FeedPortfolioRoute.serializer(), + serializer = null, handleBackButton = false, childFactory = ::bottomSheetChild, ) @@ -68,10 +68,10 @@ internal class DefaultEarnComponent( } private fun bottomSheetChild( - config: FeedPortfolioRoute, + config: FeedBottomSheetRoute, componentContext: ComponentContext, ): ComposableBottomSheetComponent = when (config) { - is FeedPortfolioRoute.AddToPortfolio -> { + is FeedBottomSheetRoute.AddToPortfolio -> { addToPortfolioComponentFactory.create( context = childByContext(componentContext), params = AddToPortfolioPreselectedDataComponent.Params( @@ -80,6 +80,14 @@ internal class DefaultEarnComponent( ), ) } + is FeedBottomSheetRoute.NetworkFilter -> EarnNetworkFilterComponent( + context = childByContext(componentContext), + params = config.params, + ) + is FeedBottomSheetRoute.TypeFilter -> EarnTypeFilterComponent( + context = childByContext(componentContext), + params = config.params, + ) } @Serializable diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/EarnNetworkFilterComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/EarnNetworkFilterComponent.kt new file mode 100644 index 0000000000..bf3b30b651 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/EarnNetworkFilterComponent.kt @@ -0,0 +1,43 @@ +package com.tangem.features.feed.components.earn + +import androidx.compose.runtime.Composable +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.earn.model.EarnFilterNetwork +import com.tangem.features.feed.model.earn.filters.EarnNetworkFilterModel +import com.tangem.features.feed.ui.earn.components.EarnFilterByNetworkBottomSheet +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +internal class EarnNetworkFilterComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: Params, +) : AppComponentContext by context, ComposableBottomSheetComponent { + + private val model = getOrCreateModel(params = params) + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val state = model.state.collectAsStateWithLifecycle() + EarnFilterByNetworkBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = state.value, + ), + ) + } + + data class Params( + val allFilters: List, + val onFilterSelected: (EarnFilterNetwork) -> Unit, + val onDismiss: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/EarnTypeFilterComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/EarnTypeFilterComponent.kt new file mode 100644 index 0000000000..de290cb1e3 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/EarnTypeFilterComponent.kt @@ -0,0 +1,44 @@ +package com.tangem.features.feed.components.earn + +import androidx.compose.runtime.Composable +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.earn.model.EarnFilterType +import com.tangem.features.feed.model.earn.filters.EarnTypeFilterModel +import com.tangem.features.feed.ui.earn.components.EarnFilterByTypeBottomSheet +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +internal class EarnTypeFilterComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: Params, +) : AppComponentContext by context, ComposableBottomSheetComponent { + + private val model = getOrCreateModel(params = params) + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val state = model.state.collectAsStateWithLifecycle() + + EarnFilterByTypeBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = state.value, + ), + ) + } + + data class Params( + val selectedFilter: EarnFilterType, + val onFilterSelected: (EarnFilterType) -> Unit, + val onDismiss: () -> Unit, + ) +} \ No newline at end of file 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 d7f9b477fa..fba9eedfbe 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 @@ -15,7 +15,9 @@ import com.tangem.core.decompose.model.getOrCreateModel 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.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 @@ -31,7 +33,7 @@ internal class DefaultFeedComponent( private val bottomSheetSlot = childSlot( source = feedComponentModel.bottomSheetNavigation, - serializer = FeedPortfolioRoute.serializer(), + serializer = null, handleBackButton = false, childFactory = ::bottomSheetChild, ) @@ -64,18 +66,20 @@ internal class DefaultFeedComponent( } private fun bottomSheetChild( - config: FeedPortfolioRoute, + config: FeedBottomSheetRoute, componentContext: ComponentContext, ): ComposableBottomSheetComponent = when (config) { - is FeedPortfolioRoute.AddToPortfolio -> { + is FeedBottomSheetRoute.AddToPortfolio -> { addToPortfolioComponentFactory.create( context = childByContext(componentContext), - params = AddToPortfolioPreselectedDataComponent.Params( + params = Params( tokenToAdd = config.tokenToAdd, callback = feedComponentModel.addToPortfolioCallback, ), ) } + is FeedBottomSheetRoute.NetworkFilter -> EmptyComposableBottomSheetComponent + is FeedBottomSheetRoute.TypeFilter -> EmptyComposableBottomSheetComponent } data class FeedParams(val feedClickIntents: FeedModelClickIntents) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt new file mode 100644 index 0000000000..6a2ad72b81 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt @@ -0,0 +1,14 @@ +package com.tangem.features.feed.components.feed + +import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent +import com.tangem.features.feed.components.earn.EarnTypeFilterComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent + +internal sealed interface FeedBottomSheetRoute { + + data class AddToPortfolio(val tokenToAdd: AddToPortfolioPreselectedDataComponent.TokenToAdd) : FeedBottomSheetRoute + + data class NetworkFilter(val params: EarnNetworkFilterComponent.Params) : FeedBottomSheetRoute + + data class TypeFilter(val params: EarnTypeFilterComponent.Params) : FeedBottomSheetRoute +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedPortfolioRoute.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedPortfolioRoute.kt deleted file mode 100644 index afaf4193dd..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedPortfolioRoute.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.features.feed.components.feed - -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent -import kotlinx.serialization.Serializable - -@Serializable -sealed interface FeedPortfolioRoute { - - @Serializable - data class AddToPortfolio(val tokenToAdd: AddToPortfolioPreselectedDataComponent.TokenToAdd) : FeedPortfolioRoute -} \ 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 f81263e79f..2fee6f7d0b 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 @@ -4,6 +4,8 @@ import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.feed.model.FeedEntryModel import com.tangem.features.feed.model.earn.EarnModel +import com.tangem.features.feed.model.earn.filters.EarnNetworkFilterModel +import com.tangem.features.feed.model.earn.filters.EarnTypeFilterModel import com.tangem.features.feed.model.feed.FeedComponentModel import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel import com.tangem.features.feed.model.market.list.MarketsListModel @@ -53,4 +55,14 @@ internal interface ModelModule { @IntoMap @ClassKey(FeedEntryModel::class) fun provideFeedEntryModel(model: FeedEntryModel): Model + + @Binds + @IntoMap + @ClassKey(EarnNetworkFilterModel::class) + fun provideEarnNetworkFilterModel(model: EarnNetworkFilterModel): Model + + @Binds + @IntoMap + @ClassKey(EarnTypeFilterModel::class) + fun provideEarnTypeFilterModel(model: EarnTypeFilterModel): 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 17eb499815..a26dd26493 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 @@ -5,14 +5,14 @@ import arrow.core.Either import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss -import com.tangem.common.R import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter 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.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.earn.model.EarnFilter +import com.tangem.domain.earn.model.EarnFilterNetwork +import com.tangem.domain.earn.model.EarnFilterType import com.tangem.domain.earn.usecase.* import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.models.currency.CryptoCurrency @@ -20,17 +20,26 @@ import com.tangem.domain.models.earn.EarnNetworks import com.tangem.domain.models.earn.EarnTokenWithCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.feed.components.earn.DefaultEarnComponent -import com.tangem.features.feed.components.feed.FeedPortfolioRoute +import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent +import com.tangem.features.feed.components.earn.EarnTypeFilterComponent +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.filters.state.EarnFilterNetworkConverter +import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConverter +import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeConverter +import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeUMConverter import com.tangem.features.feed.model.earn.state.EarnStateController -import com.tangem.features.feed.model.earn.state.converter.EarnNetworksConverter -import com.tangem.features.feed.model.earn.state.transformers.* +import com.tangem.features.feed.model.earn.state.transformers.EarnFilterSelectedStateTransformer +import com.tangem.features.feed.model.earn.state.transformers.UpdateBestOpportunitiesStateTransformer +import com.tangem.features.feed.model.earn.state.transformers.UpdateEarnUMInitialStateTransformer +import com.tangem.features.feed.model.earn.state.transformers.UpdateMostlyUsedStateTransformer import com.tangem.features.feed.model.earn.statemanager.EarnListBatchFlowManager import com.tangem.features.feed.model.earn.statemanager.EarnListStateManager -import com.tangem.features.feed.ui.earn.state.* +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM +import com.tangem.features.feed.ui.earn.state.EarnUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -46,13 +55,14 @@ internal class EarnModel @Inject constructor( private val getEarnTokensBatchFlowUseCase: GetEarnTokensBatchFlowUseCase, private val getTopEarnTokensUseCase: GetTopEarnTokensUseCase, private val fetchTopEarnTokensUseCase: FetchTopEarnTokensUseCase, + private val getEarnFilterUseCase: GetEarnFilterUseCase, + private val setEarnFilterUseCase: SetEarnFilterUseCase, private val appRouter: AppRouter, private val stateController: EarnStateController, ) : Model() { private val params = paramsContainer.require() private val earnNetworks = MutableStateFlow(Either.Right(emptyList())) - private val earnNetworksConverter: EarnNetworksConverter by lazy { EarnNetworksConverter() } private val earnListConfigProvider = Provider { createEarnTokensListConfig( selectedTypeFilter = stateController.value.selectedTypeFilter, @@ -61,17 +71,15 @@ internal class EarnModel @Inject constructor( ) } - private val batchFlowManager by lazy { - EarnListBatchFlowManager( - getEarnTokensBatchFlowUseCase = getEarnTokensBatchFlowUseCase, - configProvider = earnListConfigProvider, - onItemClick = ::onEarnTokenClick, - modelScope = modelScope, - dispatchers = dispatchers, - ) - } + private val batchFlowManager = EarnListBatchFlowManager( + getEarnTokensBatchFlowUseCase = getEarnTokensBatchFlowUseCase, + configProvider = earnListConfigProvider, + onItemClick = ::onEarnTokenClick, + modelScope = modelScope, + dispatchers = dispatchers, + ) - val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() val addToPortfolioCallback = object : AddToPortfolioPreselectedDataComponent.Callback { override fun onDismiss() = bottomSheetNavigation.dismiss() @@ -93,6 +101,7 @@ internal class EarnModel @Inject constructor( updateInitialState() fetchEarnNetworks() fetchTopEarnTokens() + subscribeOnStoredFilters() subscribeOnNetworks() subscribeOnBatchFlow() subscribeToMostlyUsed() @@ -117,9 +126,7 @@ internal class EarnModel @Inject constructor( ) }.onEach { bestOpportunitiesState -> stateController.update(UpdateBestOpportunitiesStateTransformer(bestOpportunitiesState)) - } - .onStart { batchFlowManager.reload() } - .launchIn(modelScope) + }.launchIn(modelScope) } private fun subscribeToMostlyUsed() { @@ -142,6 +149,23 @@ internal class EarnModel @Inject constructor( } } + private fun subscribeOnStoredFilters() { + modelScope.launch(dispatchers.default) { + getEarnFilterUseCase() + .collect { filter -> + val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType) + val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork) + stateController.update( + EarnFilterSelectedStateTransformer( + filterType = typeFilterUM, + filterNetwork = networkFilterUM, + ), + ) + batchFlowManager.reload() + } + } + } + private fun fetchTopEarnTokens() { modelScope.launch(dispatchers.default) { fetchTopEarnTokensUseCase() @@ -154,142 +178,73 @@ internal class EarnModel @Inject constructor( } } - private fun createTypeFilterBottomSheetConfig( - selectedOption: EarnFilterTypeUM, - isShown: Boolean = false, - ): TangemBottomSheetConfig { - return TangemBottomSheetConfig( - isShown = isShown, - onDismissRequest = ::hideTypeFilterBottomSheet, - content = EarnFilterByTypeBottomSheetContentUM( - selectedOption = selectedOption, - onOptionClick = ::onTypeFilterOptionSelected, - ), - ) - } - - private fun createNetworkFilterBottomSheetConfig( - selectedNetwork: EarnFilterNetworkUM, - isShown: Boolean = false, - ): TangemBottomSheetConfig { - val filters = buildList { - add( - EarnFilterNetworkUM.AllNetworks( - text = TextReference.Res(R.string.earn_filter_all_networks), - isSelected = false, - ), - ) - add( - EarnFilterNetworkUM.MyNetworks( - text = TextReference.Res(R.string.earn_filter_my_networks), - isSelected = false, - ), - ) - earnNetworks.value.onRight { networks -> - addAll(networks.map(earnNetworksConverter::convert)) - } - } - - val updatedNetworks = filters.map { filter -> - val shouldBeSelected = when (filter) { - is EarnFilterNetworkUM.AllNetworks -> selectedNetwork is EarnFilterNetworkUM.AllNetworks - is EarnFilterNetworkUM.MyNetworks -> selectedNetwork is EarnFilterNetworkUM.MyNetworks - is EarnFilterNetworkUM.Network -> - selectedNetwork is EarnFilterNetworkUM.Network && filter.id == selectedNetwork.id - } - - if (filter.isSelected != shouldBeSelected) { - when (filter) { - is EarnFilterNetworkUM.AllNetworks -> filter.copy(isSelected = shouldBeSelected) - is EarnFilterNetworkUM.MyNetworks -> filter.copy(isSelected = shouldBeSelected) - is EarnFilterNetworkUM.Network -> filter.copy(isSelected = shouldBeSelected) - } - } else { - filter - } - } - - return TangemBottomSheetConfig( - isShown = isShown, - onDismissRequest = ::hideNetworkFilterBottomSheet, - content = EarnFilterByNetworkBottomSheetContentUM( - selectedNetwork = selectedNetwork, - networks = persistentListOf(*updatedNetworks.toTypedArray()), - onOptionClick = ::onNetworkFilterOptionSelected, - ), - ) - } - /* start of clicks area */ private fun onTypeFilterClick() { - stateController.update( - EarnTypeFilterStateTransformer( - shouldShow = true, - filterByTypeBottomSheetConfig = ::createTypeFilterBottomSheetConfig, - ), - ) - } - - private fun hideTypeFilterBottomSheet() { - stateController.update( - EarnTypeFilterStateTransformer( - shouldShow = false, - filterByTypeBottomSheetConfig = ::createTypeFilterBottomSheetConfig, + val currentState = state.value + bottomSheetNavigation.activate( + FeedBottomSheetRoute.TypeFilter( + params = EarnTypeFilterComponent.Params( + selectedFilter = EarnFilterTypeUMConverter().convert(currentState.selectedTypeFilter), + onFilterSelected = ::onTypeFilterOptionSelected, + onDismiss = { bottomSheetNavigation.dismiss() }, + ), ), ) } private fun onNetworkFilterClick() { - stateController.update( - EarnNetworkFilterStateTransformer( - shouldShow = true, - filterByNetworkBottomSheetConfig = ::createNetworkFilterBottomSheetConfig, + bottomSheetNavigation.activate( + FeedBottomSheetRoute.NetworkFilter( + params = EarnNetworkFilterComponent.Params( + allFilters = createNetworkFilters(), + onFilterSelected = ::onNetworkFilterOptionSelected, + onDismiss = { bottomSheetNavigation.dismiss() }, + ), ), ) } - private fun hideNetworkFilterBottomSheet() { - stateController.update( - EarnNetworkFilterStateTransformer( - shouldShow = false, - filterByNetworkBottomSheetConfig = ::createNetworkFilterBottomSheetConfig, - ), - ) - } - - private fun onTypeFilterOptionSelected(type: EarnFilterTypeUM) { - stateController.update( - EarnTypeFilterSelectedStateTransformer( - filter = type, - filterByTypeBottomSheetConfig = ::createTypeFilterBottomSheetConfig, - ), - ) - batchFlowManager.reload() - } - - private fun onNetworkFilterOptionSelected(network: EarnFilterNetworkUM) { - stateController.update( - EarnNetworkFilterSelectedStateTransformer( - filter = network, - filterByNetworkBottomSheetConfig = ::createNetworkFilterBottomSheetConfig, - ), - ) - batchFlowManager.reload() + private fun createNetworkFilters(): List { + val selectedFilter = state.value.selectedNetworkFilter + return buildList { + add( + EarnFilterNetwork.AllNetworks( + isSelected = selectedFilter is EarnFilterNetworkUM.AllNetworks, + ), + ) + add( + EarnFilterNetwork.MyNetworks( + isSelected = selectedFilter is EarnFilterNetworkUM.MyNetworks, + ), + ) + earnNetworks.value.onRight { networks -> + networks.mapTo(this) { network -> + EarnFilterNetwork.Specific( + id = network.networkId, + isSelected = selectedFilter is EarnFilterNetworkUM.Network && + selectedFilter.id == network.networkId, + symbol = network.symbol, + fullName = network.fullName, + ) + } + } + } } private fun onClearFiltersClick() { - stateController.update( - ClearEarnUMFilterStateTransformer( - filterByTypeBottomSheetConfig = ::createTypeFilterBottomSheetConfig, - filterByNetworkBottomSheetConfig = ::createNetworkFilterBottomSheetConfig, - ), - ) - batchFlowManager.reload() + modelScope.launch(dispatchers.default) { + setEarnFilterUseCase( + EarnFilter( + earnFilterNetwork = EarnFilterNetwork.AllNetworks(isSelected = true), + earnFilterType = EarnFilterType.ALL, + ), + ) + } } private fun onEarnTokenClick(earnTokenWithCurrency: EarnTokenWithCurrency) { bottomSheetNavigation.activate( - FeedPortfolioRoute.AddToPortfolio( + FeedBottomSheetRoute.AddToPortfolio( tokenToAdd = AddToPortfolioPreselectedDataComponent.TokenToAdd( network = TokenMarketInfo.Network( networkId = earnTokenWithCurrency.earnToken.networkId, @@ -304,6 +259,30 @@ internal class EarnModel @Inject constructor( ), ) } + + private fun onTypeFilterOptionSelected(type: EarnFilterType) { + modelScope.launch(dispatchers.default) { + setEarnFilterUseCase( + EarnFilter( + earnFilterNetwork = EarnFilterNetworkUMConverter().convert(state.value.selectedNetworkFilter), + earnFilterType = type, + ), + ) + bottomSheetNavigation.dismiss() + } + } + + private fun onNetworkFilterOptionSelected(filter: EarnFilterNetwork) { + modelScope.launch(dispatchers.default) { + setEarnFilterUseCase( + EarnFilter( + earnFilterNetwork = filter, + earnFilterType = EarnFilterTypeUMConverter().convert(state.value.selectedTypeFilter), + ), + ) + } + bottomSheetNavigation.dismiss() + } /* end of clicks area */ private fun updateInitialState() { @@ -311,10 +290,6 @@ internal class EarnModel @Inject constructor( UpdateEarnUMInitialStateTransformer( onBackClick = params.onBackClick, onNetworkFilterClick = ::onNetworkFilterClick, - filterByTypeBottomSheetConfig = createTypeFilterBottomSheetConfig(state.value.selectedTypeFilter), - filterByNetworkBottomSheetConfig = createNetworkFilterBottomSheetConfig( - selectedNetwork = state.value.selectedNetworkFilter, - ), onTypeFilterClick = ::onTypeFilterClick, ), ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnNetworkFilterModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnNetworkFilterModel.kt new file mode 100644 index 0000000000..47f92e70d1 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnNetworkFilterModel.kt @@ -0,0 +1,37 @@ +package com.tangem.features.feed.model.earn.filters + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent +import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConverter +import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkConverter +import com.tangem.features.feed.ui.earn.state.EarnFilterByNetworkBottomSheetContentUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class EarnNetworkFilterModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, +) : Model() { + + private val params = paramsContainer.require() + + private val _state = MutableStateFlow(configureInitialState()) + val state = _state.asStateFlow() + + private fun configureInitialState(): EarnFilterByNetworkBottomSheetContentUM { + return EarnFilterByNetworkBottomSheetContentUM( + networks = params.allFilters + .map { EarnFilterNetworkConverter().convert(it) } + .toPersistentList(), + onOptionClick = { filterUM -> params.onFilterSelected(EarnFilterNetworkUMConverter().convert(filterUM)) }, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnTypeFilterModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnTypeFilterModel.kt new file mode 100644 index 0000000000..01e2454dad --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnTypeFilterModel.kt @@ -0,0 +1,36 @@ +package com.tangem.features.feed.model.earn.filters + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.feed.components.earn.EarnTypeFilterComponent +import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeConverter +import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeUMConverter +import com.tangem.features.feed.ui.earn.state.EarnFilterByTypeBottomSheetContentUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class EarnTypeFilterModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, +) : Model() { + + private val params = paramsContainer.require() + + private val _state = MutableStateFlow(configureInitialState()) + val state = _state.asStateFlow() + + private fun configureInitialState(): EarnFilterByTypeBottomSheetContentUM { + return EarnFilterByTypeBottomSheetContentUM( + selectedOption = EarnFilterTypeConverter().convert(params.selectedFilter), + onOptionClick = { filterTypeUM -> + params.onFilterSelected(EarnFilterTypeUMConverter().convert(filterTypeUM)) + }, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterNetworkConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterNetworkConverter.kt new file mode 100644 index 0000000000..62f5348e16 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterNetworkConverter.kt @@ -0,0 +1,31 @@ +package com.tangem.features.feed.model.earn.filters.state + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.domain.earn.model.EarnFilterNetwork +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.utils.converter.Converter + +internal class EarnFilterNetworkConverter : Converter { + + override fun convert(value: EarnFilterNetwork): EarnFilterNetworkUM { + return when (value) { + is EarnFilterNetwork.AllNetworks -> { + EarnFilterNetworkUM.AllNetworks(isSelected = value.isSelected) + } + is EarnFilterNetwork.MyNetworks -> { + EarnFilterNetworkUM.MyNetworks(isSelected = value.isSelected) + } + is EarnFilterNetwork.Specific -> { + EarnFilterNetworkUM.Network( + id = value.id, + text = value.fullName, + symbol = value.symbol, + iconRes = getActiveIconRes(Blockchain.fromNetworkId(value.id)?.id.orEmpty()), + isSelected = value.isSelected, + ) + } + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterNetworkUMConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterNetworkUMConverter.kt new file mode 100644 index 0000000000..5bd1a9c437 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterNetworkUMConverter.kt @@ -0,0 +1,21 @@ +package com.tangem.features.feed.model.earn.filters.state + +import com.tangem.domain.earn.model.EarnFilterNetwork +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.utils.converter.Converter + +internal class EarnFilterNetworkUMConverter : Converter { + + override fun convert(value: EarnFilterNetworkUM): EarnFilterNetwork { + return when (value) { + is EarnFilterNetworkUM.AllNetworks -> EarnFilterNetwork.AllNetworks(isSelected = value.isSelected) + is EarnFilterNetworkUM.MyNetworks -> EarnFilterNetwork.MyNetworks(isSelected = value.isSelected) + is EarnFilterNetworkUM.Network -> EarnFilterNetwork.Specific( + isSelected = value.isSelected, + id = value.id, + symbol = value.symbol, + fullName = value.text, + ) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterTypeConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterTypeConverter.kt new file mode 100644 index 0000000000..0afa48ddd4 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterTypeConverter.kt @@ -0,0 +1,15 @@ +package com.tangem.features.feed.model.earn.filters.state + +import com.tangem.domain.earn.model.EarnFilterType +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM +import com.tangem.utils.converter.Converter + +internal class EarnFilterTypeConverter : Converter { + override fun convert(value: EarnFilterType): EarnFilterTypeUM { + return when (value) { + EarnFilterType.ALL -> EarnFilterTypeUM.All + EarnFilterType.STAKING -> EarnFilterTypeUM.Staking + EarnFilterType.YIELD -> EarnFilterTypeUM.YieldMode + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterTypeUMConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterTypeUMConverter.kt new file mode 100644 index 0000000000..483c27a5d1 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterTypeUMConverter.kt @@ -0,0 +1,16 @@ +package com.tangem.features.feed.model.earn.filters.state + +import com.tangem.domain.earn.model.EarnFilterType +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM +import com.tangem.utils.converter.Converter + +internal class EarnFilterTypeUMConverter : Converter { + + override fun convert(value: EarnFilterTypeUM): EarnFilterType { + return when (value) { + EarnFilterTypeUM.All -> EarnFilterType.ALL + EarnFilterTypeUM.Staking -> EarnFilterType.STAKING + EarnFilterTypeUM.YieldMode -> EarnFilterType.YIELD + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt index 6709151fb5..4af99089f3 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,9 +1,6 @@ package com.tangem.features.feed.model.earn.state -import com.tangem.common.R import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -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 kotlinx.coroutines.flow.MutableStateFlow @@ -34,12 +31,7 @@ internal class EarnStateController @Inject constructor() { mostlyUsed = EarnListUM.Loading, bestOpportunities = EarnBestOpportunitiesUM.Loading, selectedTypeFilter = EarnFilterTypeUM.All, - selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks( - text = TextReference.Res(R.string.earn_filter_all_networks), - isSelected = true, - ), - filterByTypeBottomSheet = TangemBottomSheetConfig.Empty, - filterByNetworkBottomSheet = TangemBottomSheetConfig.Empty, + selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true), onBackClick = {}, onNetworkFilterClick = {}, onTypeFilterClick = {}, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/converter/EarnNetworksConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/converter/EarnNetworksConverter.kt deleted file mode 100644 index 3f50e4aef8..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/converter/EarnNetworksConverter.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.features.feed.model.earn.state.converter - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.getActiveIconRes -import com.tangem.domain.models.earn.EarnNetwork -import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM -import com.tangem.utils.converter.Converter - -internal class EarnNetworksConverter : Converter { - - override fun convert(value: EarnNetwork): EarnFilterNetworkUM.Network { - return EarnFilterNetworkUM.Network( - id = value.networkId, - text = TextReference.Str(value.fullName), - symbol = TextReference.Str(value.symbol), - iconRes = getActiveIconRes(Blockchain.fromNetworkId(value.networkId)?.id.orEmpty()), - isSelected = false, - ) - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/ClearEarnUMFilterStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/ClearEarnUMFilterStateTransformer.kt deleted file mode 100644 index b747c62f70..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/ClearEarnUMFilterStateTransformer.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.features.feed.model.earn.state.transformers - -import com.tangem.common.R -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM -import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM -import com.tangem.features.feed.ui.earn.state.EarnUM - -internal class ClearEarnUMFilterStateTransformer( - private val filterByTypeBottomSheetConfig: (EarnFilterTypeUM, Boolean) -> TangemBottomSheetConfig, - private val filterByNetworkBottomSheetConfig: (EarnFilterNetworkUM, Boolean) -> TangemBottomSheetConfig, -) : EarnUMTransformer { - - override fun transform(prevState: EarnUM): EarnUM { - val defaultNetworksFilter = EarnFilterNetworkUM.AllNetworks( - text = TextReference.Res(R.string.earn_filter_all_networks), - isSelected = true, - ) - - return prevState.copy( - selectedTypeFilter = EarnFilterTypeUM.All, - selectedNetworkFilter = defaultNetworksFilter, - filterByTypeBottomSheet = filterByTypeBottomSheetConfig(EarnFilterTypeUM.All, false), - filterByNetworkBottomSheet = filterByNetworkBottomSheetConfig(defaultNetworksFilter, false), - ) - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt new file mode 100644 index 0000000000..9bdb205b49 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class EarnFilterSelectedStateTransformer( + private val filterType: EarnFilterTypeUM, + private val filterNetwork: EarnFilterNetworkUM, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return prevState.copy( + selectedTypeFilter = filterType, + selectedNetworkFilter = filterNetwork, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt index a9ee578d30..a4ae7833a3 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt @@ -1,18 +1,13 @@ package com.tangem.features.feed.model.earn.state.transformers -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM import com.tangem.features.feed.ui.earn.state.EarnUM internal class EarnNetworkFilterSelectedStateTransformer( private val filter: EarnFilterNetworkUM, - private val filterByNetworkBottomSheetConfig: (EarnFilterNetworkUM, Boolean) -> TangemBottomSheetConfig, ) : EarnUMTransformer { override fun transform(prevState: EarnUM): EarnUM { - return prevState.copy( - selectedNetworkFilter = filter, - filterByNetworkBottomSheet = filterByNetworkBottomSheetConfig(filter, false), - ) + return prevState.copy(selectedNetworkFilter = filter) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterStateTransformer.kt deleted file mode 100644 index 5594b46a70..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterStateTransformer.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.features.feed.model.earn.state.transformers - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM -import com.tangem.features.feed.ui.earn.state.EarnUM - -internal class EarnNetworkFilterStateTransformer( - private val shouldShow: Boolean, - private val filterByNetworkBottomSheetConfig: (EarnFilterNetworkUM, Boolean) -> TangemBottomSheetConfig, -) : EarnUMTransformer { - - override fun transform(prevState: EarnUM): EarnUM { - return if (shouldShow) { - prevState.copy( - filterByNetworkBottomSheet = filterByNetworkBottomSheetConfig(prevState.selectedNetworkFilter, true), - ) - } else { - prevState.copy(filterByNetworkBottomSheet = prevState.filterByNetworkBottomSheet.copy(isShown = false)) - } - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterSelectedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterSelectedStateTransformer.kt deleted file mode 100644 index 3110ddfbfc..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterSelectedStateTransformer.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.feed.model.earn.state.transformers - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM -import com.tangem.features.feed.ui.earn.state.EarnUM - -internal class EarnTypeFilterSelectedStateTransformer( - private val filter: EarnFilterTypeUM, - private val filterByTypeBottomSheetConfig: (EarnFilterTypeUM, Boolean) -> TangemBottomSheetConfig, -) : EarnUMTransformer { - - override fun transform(prevState: EarnUM): EarnUM { - return prevState.copy( - selectedTypeFilter = filter, - filterByTypeBottomSheet = filterByTypeBottomSheetConfig(filter, false), - ) - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterStateTransformer.kt deleted file mode 100644 index dacdb66034..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterStateTransformer.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.features.feed.model.earn.state.transformers - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM -import com.tangem.features.feed.ui.earn.state.EarnUM - -internal class EarnTypeFilterStateTransformer( - private val shouldShow: Boolean, - private val filterByTypeBottomSheetConfig: (EarnFilterTypeUM, Boolean) -> TangemBottomSheetConfig, -) : EarnUMTransformer { - - override fun transform(prevState: EarnUM): EarnUM { - return if (shouldShow) { - prevState.copy( - filterByTypeBottomSheet = filterByTypeBottomSheetConfig(prevState.selectedTypeFilter, true), - ) - } else { - prevState.copy(filterByTypeBottomSheet = prevState.filterByTypeBottomSheet.copy(isShown = false)) - } - } -} \ 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 c726dfd6af..f60b57a8e0 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,9 @@ package com.tangem.features.feed.model.earn.state.transformers -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.features.feed.ui.earn.state.EarnUM internal class UpdateEarnUMInitialStateTransformer( private val onBackClick: () -> Unit, - private val filterByTypeBottomSheetConfig: TangemBottomSheetConfig, - private val filterByNetworkBottomSheetConfig: TangemBottomSheetConfig, private val onNetworkFilterClick: () -> Unit, private val onTypeFilterClick: () -> Unit, ) : EarnUMTransformer { @@ -14,8 +11,6 @@ internal class UpdateEarnUMInitialStateTransformer( override fun transform(prevState: EarnUM): EarnUM { return prevState.copy( onBackClick = onBackClick, - filterByNetworkBottomSheet = filterByNetworkBottomSheetConfig, - filterByTypeBottomSheet = filterByTypeBottomSheetConfig, onNetworkFilterClick = onNetworkFilterClick, onTypeFilterClick = onTypeFilterClick, ) 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 c5cbabead3..be776002e3 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 @@ -29,7 +29,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase import com.tangem.features.feed.components.feed.DefaultFeedComponent -import com.tangem.features.feed.components.feed.FeedPortfolioRoute +import com.tangem.features.feed.components.feed.FeedBottomSheetRoute import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.feed.impl.R @@ -89,7 +89,7 @@ internal class FeedComponentModel @Inject constructor( dispatchers = dispatchers, ) - val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() val addToPortfolioCallback = object : AddToPortfolioPreselectedDataComponent.Callback { override fun onDismiss() = bottomSheetNavigation.dismiss() @@ -398,7 +398,7 @@ internal class FeedComponentModel @Inject constructor( private fun handleEarnTokenClick(earnTokenWithCurrency: EarnTokenWithCurrency) { bottomSheetNavigation.activate( - FeedPortfolioRoute.AddToPortfolio( + FeedBottomSheetRoute.AddToPortfolio( tokenToAdd = AddToPortfolioPreselectedDataComponent.TokenToAdd( network = TokenMarketInfo.Network( networkId = earnTokenWithCurrency.earnToken.networkId, 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 32f85c07b6..86139c45f7 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,7 +24,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig 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 @@ -37,7 +36,9 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.feed.ui.earn.components.* +import com.tangem.features.feed.ui.earn.components.EarnItemPlaceholder +import com.tangem.features.feed.ui.earn.components.EarnListItem +import com.tangem.features.feed.ui.earn.components.MostlyUsedPlaceholder import com.tangem.features.feed.ui.earn.state.* import kotlinx.collections.immutable.persistentListOf @@ -50,9 +51,6 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() } val listState = rememberLazyListState() - EarnFilterByTypeBottomSheet(config = state.filterByTypeBottomSheet) - EarnFilterByNetworkBottomSheet(config = state.filterByNetworkBottomSheet) - if (state.bestOpportunities is EarnBestOpportunitiesUM.Content) { PaginationHandler( listState = listState, @@ -89,7 +87,11 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { SpacerH(12.dp) BestOpportunitiesFilters( state = state.bestOpportunities, - selectedNetworkFilterText = state.selectedNetworkFilterText, + selectedNetworkFilterText = when (state.selectedNetworkFilter) { + is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks) + is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks) + is EarnFilterNetworkUM.Network -> TextReference.Str(state.selectedNetworkFilter.text) + }, selectedTypeFilterText = state.selectedTypeFilterText, onNetworkFilterClick = state.onNetworkFilterClick, onTypeFilterClick = state.onTypeFilterClick, @@ -573,12 +575,7 @@ private fun previewEarnUM( mostlyUsed = mostlyUsed, bestOpportunities = bestOpportunities, selectedTypeFilter = EarnFilterTypeUM.All, - selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks( - text = resourceReference(R.string.earn_filter_all_networks), - isSelected = true, - ), - filterByTypeBottomSheet = TangemBottomSheetConfig.Empty, - filterByNetworkBottomSheet = TangemBottomSheetConfig.Empty, + selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true), onBackClick = {}, onNetworkFilterClick = {}, onTypeFilterClick = {}, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt index a04546676b..fc3da1e441 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt @@ -28,9 +28,8 @@ import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.components.rows.RowContentContainer import com.tangem.core.ui.components.rows.RowText import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.resolveReference +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.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -106,7 +105,11 @@ private fun LazyListScope.allMyNetworksList( showDivider = index != allMyNetworks.lastIndex, ) { InputRowChecked( - text = item.text, + text = when (item) { + is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks) + is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks) + is EarnFilterNetworkUM.Network -> TextReference.Str(item.text) + }, checked = item.isSelected, ) } @@ -170,13 +173,13 @@ private fun LazyListScope.specificNetworksList( Image( modifier = Modifier.size(22.dp), imageVector = ImageVector.vectorResource(item.iconRes), - contentDescription = item.symbol.resolveReference(), + contentDescription = item.symbol, ) }, text = { RowText( - mainText = item.text.resolveReference(), - secondText = item.symbol.resolveReference(), + mainText = item.text, + secondText = item.symbol, accentMainText = true, accentSecondText = false, ) @@ -208,30 +211,20 @@ private fun Preview() { isShown = true, onDismissRequest = {}, content = EarnFilterByNetworkBottomSheetContentUM( - selectedNetwork = EarnFilterNetworkUM.AllNetworks( - text = resourceReference(R.string.earn_filter_all_networks), - isSelected = true, - ), networks = persistentListOf( - EarnFilterNetworkUM.AllNetworks( - text = resourceReference(R.string.earn_filter_all_networks), - isSelected = true, - ), - EarnFilterNetworkUM.MyNetworks( - text = stringReference("My network"), - isSelected = false, - ), + EarnFilterNetworkUM.AllNetworks(isSelected = true), + EarnFilterNetworkUM.MyNetworks(isSelected = false), EarnFilterNetworkUM.Network( id = "ethereum", - text = stringReference("Ethereum"), - symbol = stringReference("ETH"), + text = "Ethereum", + symbol = "ETH", iconRes = R.drawable.img_btc_22, isSelected = false, ), EarnFilterNetworkUM.Network( id = "polygon", - text = stringReference("Polygon"), - symbol = stringReference("MATIC"), + text = "Polygon", + symbol = "MATIC", iconRes = R.drawable.img_btc_22, isSelected = false, ), diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByNetworkBottomSheetContentUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByNetworkBottomSheetContentUM.kt index 101b80fbce..585cf7aa3c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByNetworkBottomSheetContentUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByNetworkBottomSheetContentUM.kt @@ -4,7 +4,6 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import kotlinx.collections.immutable.ImmutableList internal data class EarnFilterByNetworkBottomSheetContentUM( - val selectedNetwork: EarnFilterNetworkUM, val networks: ImmutableList, val onOptionClick: (EarnFilterNetworkUM) -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt index bb22ced386..207fecdc5c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt @@ -1,29 +1,25 @@ package com.tangem.features.feed.ui.earn.state import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference @Immutable internal sealed class EarnFilterNetworkUM { - abstract val text: TextReference abstract val isSelected: Boolean data class AllNetworks( - override val text: TextReference, override val isSelected: Boolean, ) : EarnFilterNetworkUM() data class MyNetworks( - override val text: TextReference, override val isSelected: Boolean, ) : EarnFilterNetworkUM() data class Network( - val id: String, - override val text: TextReference, - val symbol: TextReference, - val iconRes: Int, override val isSelected: Boolean, + val id: String, + val text: String, + val symbol: String, + val iconRes: Int, ) : EarnFilterNetworkUM() } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt index a0b6870d0a..b0686ee0e8 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,7 +1,6 @@ package com.tangem.features.feed.ui.earn.state import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference @Immutable @@ -10,16 +9,11 @@ internal data class EarnUM( val bestOpportunities: EarnBestOpportunitiesUM, val selectedTypeFilter: EarnFilterTypeUM, val selectedNetworkFilter: EarnFilterNetworkUM, - val filterByTypeBottomSheet: TangemBottomSheetConfig, - val filterByNetworkBottomSheet: TangemBottomSheetConfig, val onBackClick: () -> Unit, val onNetworkFilterClick: () -> Unit, val onTypeFilterClick: () -> Unit, ) { - val selectedNetworkFilterText: TextReference - get() = selectedNetworkFilter.text - val selectedTypeFilterText: TextReference get() = selectedTypeFilter.text } \ No newline at end of file From 4c8af6b8e905437894b12c30f93da4986716f90a Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 10:33:18 +0300 Subject: [PATCH 042/106] Updated on 2026-08-14 --- .../v2/send/analytics/SendAnalyticEvents.kt | 7 +++++-- .../v2/send/analytics/SendAnalyticHelper.kt | 20 +++++++++++-------- .../v2/send/confirm/model/SendConfirmModel.kt | 16 ++++++++------- .../analytics/SendWithSwapAnalyticEvents.kt | 3 +++ .../confirm/model/SendWithSwapConfirmModel.kt | 11 ++++++++-- 5 files changed, 38 insertions(+), 19 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt index eab26f440b..abf8b6eaa0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt @@ -3,6 +3,7 @@ package com.tangem.features.send.v2.send.analytics 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.BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.ENS_ADDRESS import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TOKEN @@ -29,14 +30,16 @@ internal sealed class SendAnalyticEvents( val isNonceNotEmpty: Boolean, private val ensStatus: AnalyticsParam.EmptyFull, private val feeToken: String, - val derivationIndex: Int?, + private val fromDerivationIndex: Int?, + private val toDerivationIndex: Int?, ) : SendAnalyticEvents( event = "Transaction Sent Screen Opened", params = buildMap { put(TOKEN_PARAM, token) put(FEE_TYPE, feeType.value) put(BLOCKCHAIN, blockchain) - if (derivationIndex != null) put(ACCOUNT_DERIVATION_FROM, derivationIndex.toString()) + if (fromDerivationIndex != null) put(ACCOUNT_DERIVATION_FROM, fromDerivationIndex.toString()) + if (toDerivationIndex != null) put(ACCOUNT_DERIVATION_TO, toDerivationIndex.toString()) put(NONCE, isNonceNotEmpty.toString().capitalize()) val ensAddress = when (ensStatus) { AnalyticsParam.EmptyFull.Empty -> false.toString().capitalize() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt index 7ddfee6d8f..c3d536b383 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt @@ -4,7 +4,9 @@ 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.domain.account.status.usecase.GetAccountCurrencyByAddressUseCase import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.send.v2.api.entity.FeeNonce import com.tangem.features.send.v2.api.entity.FeeSelectorUM @@ -16,9 +18,10 @@ import javax.inject.Inject @ModelScoped internal class SendAnalyticHelper @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, + private val getAccountCurrencyByAddressUseCase: GetAccountCurrencyByAddressUseCase, ) { - fun sendSuccessAnalytics( + suspend fun sendSuccessAnalytics( cryptoCurrency: CryptoCurrency, sendUM: SendUM, feeToken: CryptoCurrency, @@ -28,11 +31,11 @@ internal class SendAnalyticHelper @Inject constructor( val feeSelectorUM = sendUM.feeSelectorUM as? FeeSelectorUM.Content ?: return val feeType = feeSelectorUM.toAnalyticType() val feeTokenSymbol = feeToken.symbol - val derivationIndex = when (account) { - is Account.CryptoPortfolio -> if (!account.isMainAccount) account.derivationIndex else null - is Account.Payment -> null - null -> null - } + val fromDerivationIndex = account?.derivationIndex?.value + val destination = destinationUM?.addressTextField?.actualAddress ?: return + val destinationAccount = getAccountCurrencyByAddressUseCase(destination) + .getOrNull()?.account + val toDerivationIndex = destinationAccount?.derivationIndex?.value analyticsEventHandler.send( SendAnalyticEvents.TransactionScreenOpened( token = cryptoCurrency.symbol, @@ -40,7 +43,8 @@ internal class SendAnalyticHelper @Inject constructor( blockchain = cryptoCurrency.network.name, isNonceNotEmpty = feeSelectorUM.feeNonce is FeeNonce.Nonce, ensStatus = getEnsStatus(sendUM), - derivationIndex = derivationIndex?.value, + fromDerivationIndex = fromDerivationIndex, + toDerivationIndex = toDerivationIndex, feeToken = feeTokenSymbol, ), ) @@ -52,7 +56,7 @@ internal class SendAnalyticHelper @Inject constructor( feeType = feeType, feeToken = feeTokenSymbol, ), - memoType = getSendTransactionMemoType(destinationUM?.memoTextField), + memoType = getSendTransactionMemoType(destinationUM.memoTextField), ), ) } 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 a18cc27acf..2286922911 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 @@ -24,7 +24,6 @@ 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.domain.models.wallet.isHotWallet import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -35,6 +34,7 @@ import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType 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.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.settings.NeverShowTapHelpUseCase import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase @@ -435,12 +435,14 @@ internal class SendConfirmModel @Inject constructor( updateTransactionStatus(txData, txHash) addTokenToWalletIfNeeded() sendBalanceUpdater.scheduleUpdates() - sendAnalyticHelper.sendSuccessAnalytics( - cryptoCurrency = cryptoCurrency, - sendUM = uiState.value, - account = params.accountFlow.value, - feeToken = feeToken, - ) + modelScope.launch(dispatchers.default) { + sendAnalyticHelper.sendSuccessAnalytics( + cryptoCurrency = cryptoCurrency, + sendUM = uiState.value, + account = params.accountFlow.value, + feeToken = feeToken, + ) + } params.callback.onResult(uiState.value) params.onSendTransaction() }, 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 213c7d7b63..64e5650b5b 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 @@ -3,6 +3,7 @@ package com.tangem.features.swap.v2.impl.sendviaswap.analytics 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.FEE_TYPE import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_BLOCKCHAIN @@ -24,6 +25,7 @@ internal sealed class SendWithSwapAnalyticEvents( val fromToken: CryptoCurrency, val toToken: CryptoCurrency, val fromDerivationIndex: Int?, + val toDerivationIndex: Int?, ) : SendWithSwapAnalyticEvents( event = "Send With Swap In Progress Screen Opened", params = buildMap { @@ -34,6 +36,7 @@ internal sealed class SendWithSwapAnalyticEvents( put(SEND_BLOCKCHAIN, fromToken.network.name) put(RECEIVE_BLOCKCHAIN, toToken.network.name) if (fromDerivationIndex != null) put(ACCOUNT_DERIVATION_FROM, fromDerivationIndex.toString()) + if (toDerivationIndex != null) put(ACCOUNT_DERIVATION_TO, toDerivationIndex.toString()) }, ), AppsFlyerIncludedEvent 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 9de68cfb9a..ca0ed8b981 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 @@ -22,6 +22,7 @@ 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.wrappedList +import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCase import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.models.account.derivationIndex @@ -92,6 +93,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( private val swapNotificationsUpdateTrigger: SwapNotificationsUpdateTrigger, private val sendNotificationsUpdateListener: SendNotificationsUpdateListener, private val swapNotificationsUpdateListener: SwapNotificationsUpdateListener, + private val getAccountCurrencyByAddressUseCase: GetAccountCurrencyByAddressUseCase, private val swapAmountReduceTrigger: SwapAmountReduceTrigger, private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger, private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, @@ -347,7 +349,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( txHash = txHash, currency = primaryCurrencyStatus.currency, ).getOrNull().orEmpty() - sendSuccessAnalytics() + modelScope.launch(dispatchers.default) { sendSuccessAnalytics() } uiState.transformerUpdate( SendWithSwapConfirmSentStateTransformer( timestamp = timestamp, @@ -454,13 +456,17 @@ internal class SendWithSwapConfirmModel @Inject constructor( }.launchIn(modelScope) } - private fun sendSuccessAnalytics() { + private suspend fun sendSuccessAnalytics() { val selectedProvider = confirmData.quote?.provider ?: return val fromCurrency = confirmData.fromCryptoCurrencyStatus?.currency ?: return val toCurrency = confirmData.toCryptoCurrencyStatus?.currency ?: return val feeSelectorUM = uiState.value.feeSelectorUM as? FeeSelectorUM.Content ?: return val feeType = feeSelectorUM.toAnalyticType() val fromDerivationIndex = confirmData.fromAccount?.derivationIndex?.value + val destination = destinationUM?.addressTextField?.actualAddress ?: return + val destinationAccount = getAccountCurrencyByAddressUseCase(destination) + .getOrNull()?.account + val toDerivationIndex = destinationAccount?.derivationIndex?.value analyticsEventHandler.send( SendWithSwapAnalyticEvents.TransactionScreenOpened( @@ -469,6 +475,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( fromToken = fromCurrency, toToken = toCurrency, fromDerivationIndex = fromDerivationIndex, + toDerivationIndex = toDerivationIndex, ), ) analyticsEventHandler.send( From 147243143564f4b691222640e8d9550c1e8bd167 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 11:50:44 +0400 Subject: [PATCH 043/106] Updated on 2026-08-14 --- .../data/UserWalletsStoreRepositoryProxy.kt | 53 ----------------- .../tap/di/data/UserWalletsStoreModule.kt | 21 ------- .../DefaultUserWalletsListRepository.kt | 17 ------ .../repository/UserWalletsKeysRepository.kt | 39 ------------ .../local/userwallet/UserWalletsStore.kt | 28 --------- .../AccountConverterFactoryContainer.kt | 1 + .../AccountListCryptoCurrenciesProducer.kt | 1 + .../DefaultMultiAccountListProducer.kt | 1 + ...aultMultiWalletCryptoCurrenciesProducer.kt | 1 + .../producer/WalletAccountListFlowFactory.kt | 1 + .../DefaultWalletAccountsResponseFactory.kt | 1 + .../DefaultMultiAccountListProducerTest.kt | 59 +++++++++++-------- ...MultiWalletCryptoCurrenciesProducerTest.kt | 37 ++++++++---- .../WalletAccountListFlowFactoryTest.kt | 21 +++++-- ...efaultWalletAccountsResponseFactoryTest.kt | 26 +++++--- .../DefaultCardCryptoCurrencyFactory.kt | 1 + .../data/common/currency/UserTokensSaver.kt | 1 + .../wallet/DefaultWalletServerBinder.kt | 1 + .../DefaultCardCryptoCurrencyFactoryTest.kt | 16 +++-- .../common/currency/UserTokensSaverTest.kt | 11 +++- .../wallet/DefaultWalletServerBinderTest.kt | 27 +++++---- .../express/DefaultExpressServiceFetcher.kt | 1 + .../DefaultCustomTokensRepository.kt | 1 + .../DefaultManageTokensRepository.kt | 1 + .../markets/DefaultMarketsTokenRepository.kt | 1 + .../DefaultMultiNetworkStatusProducer.kt | 1 + .../DefaultMultiNetworkStatusProducerTest.kt | 35 +++++++---- .../tangem/data/nft/DefaultNFTRepository.kt | 1 + .../data/onramp/DefaultHotCryptoRepository.kt | 5 +- .../DefaultMultiStakingBalanceFetcher.kt | 1 + .../DefaultMultiStakingBalanceFetcherTest.kt | 57 +++++++++++------- .../AccountListCryptoCurrenciesFetcher.kt | 1 + ...faultMultiWalletCryptoCurrenciesFetcher.kt | 1 + .../repository/DefaultCurrenciesRepository.kt | 2 + .../AccountListCryptoCurrenciesFetcherTest.kt | 34 ++++++++--- ...tMultiWalletCryptoCurrenciesFetcherTest.kt | 50 +++++++++++----- .../repository/DefaultTxHistoryRepository.kt | 1 + .../tangem/data/visa/DefaultVisaRepository.kt | 1 + .../data/visa/utils/VisaApiRequestMaker.kt | 1 + .../DefaultWalletConnectRepository.kt | 1 + .../DefaultWalletManagersFacade.kt | 1 + .../data/wallets/DefaultWalletsRepository.kt | 2 + .../DefaultDerivationsRepository.kt | 1 + .../hot/DefaultHotMapDerivationsRepository.kt | 1 + .../DefaultDerivationsRepositoryTest.kt | 27 ++++++--- .../usecase/IsAccountsModeEnabledUseCase.kt | 1 + .../IsAccountsModeEnabledUseCaseTest.kt | 55 +++++++---------- .../DefaultMultiAccountStatusListProducer.kt | 1 + .../DefaultSingleAccountStatusListProducer.kt | 1 + .../GetCryptoCurrencyActionsUseCaseV2.kt | 1 + ...faultMultiAccountStatusListProducerTest.kt | 40 ++++++++----- .../wallets/UserWalletsListRepository.kt | 10 ---- .../wallets/UserWalletsListRepositoryExt.kt | 20 +++++++ .../referral/data/ReferralRepositoryImpl.kt | 1 + 54 files changed, 370 insertions(+), 352 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt delete mode 100644 app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt diff --git a/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt b/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt deleted file mode 100644 index f167597c1a..0000000000 --- a/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.tangem.tap.data - -import com.tangem.common.CompletionResult -import com.tangem.common.catching -import com.tangem.datasource.local.userwallet.UserWalletsStore -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.flow - -class UserWalletsStoreRepositoryProxy( - private val userWalletsListRepository: UserWalletsListRepository, -) : UserWalletsStore { - - override val selectedUserWalletOrNull: UserWallet? - get() = userWalletsListRepository.selectedUserWallet.value - - override val userWallets: Flow> - get() = flow { - userWalletsListRepository.load() - userWalletsListRepository.userWallets.collect { - emit(requireNotNull(it)) - } - } - - override val userWalletsSync: List - get() = userWalletsListRepository.userWallets.value.orEmpty() - - override fun getSyncOrNull(key: UserWalletId): UserWallet? { - return userWalletsListRepository.userWallets.value?.find { it.walletId == key } - } - - override fun getSyncStrict(key: UserWalletId): UserWallet { - return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" } - } - - override suspend fun update( - userWalletId: UserWalletId, - update: suspend (UserWallet) -> UserWallet, - ): CompletionResult { - return catching { - val userWallet = userWalletsListRepository.userWallets.value?.find { it.walletId == userWalletId } - requireNotNull(userWallet) { "Unable to find user wallet with provided ID: $userWalletId" } - val updatedUserWallet = update(userWallet) - userWalletsListRepository.saveWithoutLock( - userWallet = updatedUserWallet, - canOverride = true, - ) - updatedUserWallet - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt deleted file mode 100644 index 8b9dae8020..0000000000 --- a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.tap.di.data - -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.tap.data.UserWalletsStoreRepositoryProxy -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 UserWalletsStoreModule { - - @Provides - @Singleton - fun provideUserWalletsStore(userWalletsListRepository: UserWalletsListRepository): UserWalletsStore { - return UserWalletsStoreRepositoryProxy(userWalletsListRepository) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index d9541b317a..a45ca9387a 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -38,9 +38,7 @@ import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.extensions.addOrReplace import com.tangem.utils.extensions.indexOfFirstOrNull import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.update import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -69,14 +67,6 @@ internal class DefaultUserWalletsListRepository( private val mutex = Mutex() - override fun getSyncOrNull(id: UserWalletId): UserWallet? { - return userWallets.value?.find { it.walletId == id } - } - - override fun getSyncStrict(id: UserWalletId): UserWallet { - return requireNotNull(getSyncOrNull(id)) { "Unable to find user wallet with provided ID: $id" } - } - override suspend fun load() { mutex.withLock { if (userWallets.value != null) return @@ -111,13 +101,6 @@ internal class DefaultUserWalletsListRepository( } } - override fun loadAndGet(): Flow> = flow { - load() - userWallets.collect { - emit(requireNotNull(it)) - } - } - override suspend fun userWalletsSync(): List { load() return requireNotNull(userWallets.value) { diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt deleted file mode 100644 index 0c196308db..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.tap.domain.userWalletList.repository - -import com.tangem.common.CompletionResult -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey - -internal interface UserWalletsKeysRepository { - /** - * Obtaining the encryption keys of all user wallets from the biometric vault. Biometric authentication required - * If that operation runs more than biometric cipher key expiration time then the user will not receive all - * encryption keys - * @return [CompletionResult] of operation with stored [UserWalletEncryptionKey] list - * */ - suspend fun getAll(): CompletionResult> - - /** - * Save the encryption key for user wallet. Biometric authentication not required - * @param encryptionKey [UserWalletEncryptionKey] to save - * @return [CompletionResult] of operation - * */ - suspend fun save(encryptionKey: UserWalletEncryptionKey): CompletionResult - - /** - * Delete encryption keys for user wallets. Biometric authentication not required - * @param userWalletsIds List of [UserWalletId] whose encryption keys will be deleted - * */ - suspend fun delete(userWalletsIds: List) - - /** - * Clear all encryption keys for user wallets. Biometric authentication not required - * */ - suspend fun clear() - - /** - * Determine if the user has saved user wallets - * @return [Boolean] true if user has saved wallets - * */ - fun hasSavedEncryptionKeys(): Boolean -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt deleted file mode 100644 index 8f00255449..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.datasource.local.userwallet - -import com.tangem.common.CompletionResult -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.coroutines.flow.Flow - -@Deprecated( - message = "Use UserWalletsListRepository instead", - replaceWith = ReplaceWith("UserWalletsListRepository"), -) -interface UserWalletsStore { - - val selectedUserWalletOrNull: UserWallet? - - val userWallets: Flow> - - val userWalletsSync: List - - fun getSyncOrNull(key: UserWalletId): UserWallet? - - fun getSyncStrict(key: UserWalletId): UserWallet - - suspend fun update( - userWalletId: UserWalletId, - update: suspend (UserWallet) -> UserWallet, - ): CompletionResult -} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt index 95de2c8111..44a347b369 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt @@ -1,6 +1,7 @@ package com.tangem.data.account.converter import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.models.wallet.UserWalletId import javax.inject.Inject diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt index 305d08e560..795c6dbc37 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt @@ -5,6 +5,7 @@ import arrow.core.some import com.tangem.data.account.store.AccountsResponseStoreFactory import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory 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.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt index 39dc1c5d19..ac2bb1b720 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt @@ -5,6 +5,7 @@ import arrow.core.some import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.MultiAccountListProducer import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.loadAndGet import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.wallet.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducer.kt index 6476babc80..16fa1c9e44 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducer.kt @@ -5,6 +5,7 @@ import arrow.core.some import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.datasource.local.token.UserTokensResponseStore 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.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt index 624eeb6679..a53f2c2e6d 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt @@ -7,6 +7,7 @@ import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.domain.account.models.AccountList import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.common.wallets.UserWalletsListRepository +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.models.wallet.isMultiCurrency diff --git a/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt index e49529c64f..8712b7eae6 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt @@ -8,6 +8,7 @@ import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResp import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.domain.account.models.AccountList import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncOrNull import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.DerivationIndex diff --git a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt index d080bf325f..5a3723c4ad 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt @@ -27,7 +27,7 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultMultiAccountListProducerTest { - private val userWalletsListRepository: UserWalletsListRepository = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) private val walletAccountListFlowFactory: WalletAccountListFlowFactory = mockk() private val flowProducerTools: FlowProducerTools = mockk() @@ -53,7 +53,7 @@ class DefaultMultiAccountListProducerTest { fun produce() = runTest { // Arrange val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsListRepository.loadAndGet() } returns userWalletsFlow + every { userWalletsListRepository.userWallets } returns userWalletsFlow val accountList = AccountList.empty(userWalletId) every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList) @@ -65,8 +65,9 @@ class DefaultMultiAccountListProducerTest { val expected = listOf(accountList) Truth.assertThat(actual).containsExactly(expected) - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsListRepository.loadAndGet() + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets walletAccountListFlowFactory.create(userWalletId) } } @@ -75,7 +76,7 @@ class DefaultMultiAccountListProducerTest { fun `flow will updated if factoryFlow is updated`() = runTest { // Arrange val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsListRepository.loadAndGet() } returns userWalletsFlow + every { userWalletsListRepository.userWallets } returns userWalletsFlow val accountList = AccountList.empty(userWalletId) val updatedAccountList = AccountList.empty(userWalletId = userWalletId, sortType = TokensSortType.NONE) @@ -97,10 +98,12 @@ class DefaultMultiAccountListProducerTest { // Assert (second emission) Truth.assertThat(secondEmission).containsExactly(listOf(updatedAccountList)) - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsListRepository.loadAndGet() + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets walletAccountListFlowFactory.create(userWalletId) - userWalletsListRepository.loadAndGet() + userWalletsListRepository.load() + userWalletsListRepository.userWallets walletAccountListFlowFactory.create(userWalletId) } } @@ -109,7 +112,7 @@ class DefaultMultiAccountListProducerTest { fun `flow is filtered the same response`() = runTest { // Arrange val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsListRepository.loadAndGet() } returns userWalletsFlow + every { userWalletsListRepository.userWallets } returns userWalletsFlow val accountList = AccountList.empty(userWalletId) val factoryFlow = MutableStateFlow(null) @@ -130,10 +133,12 @@ class DefaultMultiAccountListProducerTest { // Assert (second emission) Truth.assertThat(secondEmission).containsExactly(listOf(accountList)) - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsListRepository.loadAndGet() + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets walletAccountListFlowFactory.create(userWalletId) - userWalletsListRepository.loadAndGet() + userWalletsListRepository.load() + userWalletsListRepository.userWallets walletAccountListFlowFactory.create(userWalletId) } } @@ -143,7 +148,7 @@ class DefaultMultiAccountListProducerTest { fun `flow returns empty list if factory throws exception`() = runTest { // Arrange val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsListRepository.loadAndGet() } returns userWalletsFlow + every { userWalletsListRepository.userWallets } returns userWalletsFlow val exception = RuntimeException("Converter error") every { walletAccountListFlowFactory.create(userWalletId) } throws exception @@ -155,8 +160,9 @@ class DefaultMultiAccountListProducerTest { val expected = emptyList() Truth.assertThat(actual).containsExactly(expected) - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsListRepository.loadAndGet() + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets walletAccountListFlowFactory.create(userWalletId) } } @@ -164,8 +170,8 @@ class DefaultMultiAccountListProducerTest { @Test fun `flow is empty if userWalletsFlow returns empty flow`() = runTest { // Arrange - val userWalletsFlow = emptyFlow>() - every { userWalletsListRepository.loadAndGet() } returns userWalletsFlow + val userWalletsFlow = MutableStateFlow>(emptyList()) + every { userWalletsListRepository.userWallets } returns userWalletsFlow // Act val actual = producer.produce().let(::getEmittedValues) @@ -173,7 +179,10 @@ class DefaultMultiAccountListProducerTest { // Assert Truth.assertThat(actual).isEmpty() // no emissions - coVerify(exactly = 1) { userWalletsListRepository.loadAndGet() } + coVerify(exactly = 1) { + userWalletsListRepository.load() + userWalletsListRepository.userWallets + } coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) } } @@ -181,7 +190,7 @@ class DefaultMultiAccountListProducerTest { fun `flow is empty if factory returns empty flow`() = runTest { // Arrange val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsListRepository.loadAndGet() } returns userWalletsFlow + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { walletAccountListFlowFactory.create(userWalletId) } returns emptyFlow() @@ -191,8 +200,9 @@ class DefaultMultiAccountListProducerTest { // Assert Truth.assertThat(actual).isEmpty() // no emissions - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsListRepository.loadAndGet() + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets walletAccountListFlowFactory.create(userWalletId) } } @@ -206,7 +216,7 @@ class DefaultMultiAccountListProducerTest { } val userWalletsFlow = MutableStateFlow(listOf(userWallet, userWallet2)) - every { userWalletsListRepository.loadAndGet() } returns userWalletsFlow + every { userWalletsListRepository.userWallets } returns userWalletsFlow val accountList = AccountList.empty(userWalletId) every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList) @@ -218,8 +228,9 @@ class DefaultMultiAccountListProducerTest { // Assert Truth.assertThat(actual).isEmpty() // no emissions - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsListRepository.loadAndGet() + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets walletAccountListFlowFactory.create(userWalletId) walletAccountListFlowFactory.create(userWalletId2) } diff --git a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducerTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducerTest.kt index a923d82c5c..d1df425b53 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducerTest.kt @@ -59,7 +59,9 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { // Arrange val userTokensResponseFlow = flowOf(null) - every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow // Act @@ -72,7 +74,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual.first()).isEqualTo(expected) verifyOrder { - userWalletsListRepository.getSyncStrict(params.userWalletId) + userWalletsListRepository.userWallets userTokensResponseStore.get(params.userWalletId) } @@ -113,7 +115,9 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin), ) - every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow every { @@ -146,7 +150,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder { - userWalletsListRepository.getSyncStrict(params.userWalletId) + userWalletsListRepository.userWallets userTokensResponseStore.get(params.userWalletId) responseCryptoCurrenciesFactory.createCurrencies( response = userTokensResponse, @@ -188,7 +192,9 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { val cryptoCurrencies = emptySet() - every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow every { @@ -213,7 +219,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder { - userWalletsListRepository.getSyncStrict(params.userWalletId) + userWalletsListRepository.userWallets userTokensResponseStore.get(params.userWalletId) responseCryptoCurrenciesFactory.createCurrencies( response = userTokensResponse, @@ -257,7 +263,9 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { } .buffer(capacity = 5) - every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow every { @@ -279,7 +287,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder { - userWalletsListRepository.getSyncStrict(params.userWalletId) + userWalletsListRepository.userWallets userTokensResponseStore.get(params.userWalletId) } @@ -304,7 +312,9 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { @Test fun `flow is empty if store returns empty flow`() = runTest { // Arrange - every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { userTokensResponseStore.get(params.userWalletId) } returns emptyFlow() // Act @@ -316,7 +326,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual.first()).isEqualTo(expected) verifyOrder { - userWalletsListRepository.getSyncStrict(params.userWalletId) + userWalletsListRepository.userWallets userTokensResponseStore.get(params.userWalletId) } @@ -329,10 +339,13 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { fun `produce throws exception if UserWallet isn't multi-currency wallet`() = runTest { // Arrange val mockUserWallet = mockk { + every { walletId } returns userWallet.walletId every { isMultiCurrency } returns false } - every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow // Act val actual = runCatching { producer.produce() }.exceptionOrNull() @@ -345,7 +358,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual).isInstanceOf(expected::class.java) Truth.assertThat(actual).hasMessageThat().isEqualTo(expected.message) - verifyOrder { userWalletsListRepository.getSyncStrict(params.userWalletId) } + verifyOrder { userWalletsListRepository.userWallets } verify(inverse = true) { userTokensResponseStore.get(any()) diff --git a/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt index 61d285462f..e0abc97e2a 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt @@ -70,7 +70,9 @@ class WalletAccountListFlowFactoryTest { every { this@mockk.isMultiCurrency } returns true } - every { userWalletsListRepository.getSyncStrict(userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val accountsResponse = createGetWalletAccountsResponse(userWalletId) every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore @@ -88,7 +90,8 @@ class WalletAccountListFlowFactoryTest { val expected = accountList Truth.assertThat(actual).containsExactly(expected) - coVerify(ordering = Ordering.SEQUENCE) { + coVerifySequence { + userWalletsListRepository.userWallets accountsResponseStoreFactory.create(userWalletId) accountsResponseStore.data accountListConverterFactory.create(userWallet) @@ -105,7 +108,9 @@ class WalletAccountListFlowFactoryTest { fun `create for single wallet`() = runTest { val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false) - every { userWalletsListRepository.getSyncStrict(userWallet.walletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val currency = cryptoCurrencyFactory.ethereum every { cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet) } returns currency @@ -117,11 +122,12 @@ class WalletAccountListFlowFactoryTest { val expected = AccountList.empty(userWalletId = userWallet.walletId, cryptoCurrencies = setOf(currency)) Truth.assertThat(actual).containsExactly(expected) - coVerify(ordering = Ordering.SEQUENCE) { + coVerifySequence { cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet) } coVerify(inverse = true) { + userWalletsListRepository.userWallets cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = any()) accountsResponseStoreFactory.create(any()) accountsResponseStore.data @@ -134,7 +140,9 @@ class WalletAccountListFlowFactoryTest { fun `flow is created for single wallet with token`() = runTest { val nodl = MockUserWalletFactory.createSingleWalletWithToken() - every { userWalletsListRepository.getSyncStrict(nodl.walletId) } returns nodl + val userWalletsFlow = MutableStateFlow(listOf(nodl)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() every { @@ -148,7 +156,8 @@ class WalletAccountListFlowFactoryTest { val expected = AccountList.empty(userWalletId = nodl.walletId, cryptoCurrencies = currencies) Truth.assertThat(actual).containsExactly(expected) - coVerify(ordering = Ordering.SEQUENCE) { + coVerifySequence { + userWalletsListRepository.userWallets cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = nodl) } diff --git a/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt index 6ed7920120..afb50f75c3 100644 --- a/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt @@ -13,7 +13,11 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.account.Account import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import io.mockk.* +import io.mockk.clearMocks +import io.mockk.coVerifyOrder +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -63,7 +67,9 @@ class DefaultWalletAccountsResponseFactoryTest { tokens = emptyList(), ) - coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns null + val userWalletsFlow = MutableStateFlow?>(null) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { userTokensResponseFactory.createDefaultResponse( userWallet = null, @@ -89,7 +95,7 @@ class DefaultWalletAccountsResponseFactoryTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - userWalletsListRepository.getSyncOrNull(userWalletId) + userWalletsListRepository.userWallets userTokensResponseFactory.createDefaultResponse( userWallet = null, networkFactory = networkFactory, @@ -105,7 +111,9 @@ class DefaultWalletAccountsResponseFactoryTest { every { walletId } returns userWalletId } - coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val accounts = AccountList.empty(userWallet.walletId).accounts .filterIsInstance() @@ -145,7 +153,7 @@ class DefaultWalletAccountsResponseFactoryTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - userWalletsListRepository.getSyncOrNull(userWalletId) + userWalletsListRepository.userWallets cryptoPortfolioConverter.convertListBack(accounts) userTokensResponseFactory.createDefaultResponse( userWallet = userWallet, @@ -165,7 +173,9 @@ class DefaultWalletAccountsResponseFactoryTest { val accounts = AccountList.empty(userWallet.walletId).accounts .filterIsInstance() - coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val defaultResponse = UserTokensResponse( group = UserTokensResponse.GroupType.NETWORK, @@ -206,7 +216,9 @@ class DefaultWalletAccountsResponseFactoryTest { val userWallet = mockk(relaxed = true) { every { walletId } returns userWalletId } - coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val userTokensResponse = UserTokensResponse( group = UserTokensResponse.GroupType.NETWORK, diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt index c10ff92538..a55c9c0da8 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt @@ -9,6 +9,7 @@ import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency 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 f203b6bb3c..cff0ae1d6d 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 @@ -13,6 +13,7 @@ import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository +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 diff --git a/data/common/src/main/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinder.kt b/data/common/src/main/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinder.kt index ee977ee128..57272ffb53 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinder.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinder.kt @@ -5,6 +5,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.common.wallets.UserWalletsListRepository +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 diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt index 28aafd0069..eb90a1efb0 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt @@ -24,6 +24,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.test.core.ProvideTestModels import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested @@ -78,11 +79,12 @@ internal class DefaultCardCryptoCurrencyFactoryTest { fun `create currencies in ETH for multi-currency wallet`(model: CreateTestModel.MultiWallet) = runTest { // Arrange val userWallet = createMultiWallet() + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) val userTokensResponse = model.userTokensResponse val network = ethereum.network every { accountsFeatureToggles.isFeatureEnabled } returns false - coEvery { userWalletsListRepository.getSyncStrict(id = userWallet.walletId) } returns userWallet + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse // Act @@ -94,7 +96,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - userWalletsListRepository.getSyncStrict(id = userWallet.walletId) + userWalletsListRepository.userWallets userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId) } } @@ -130,8 +132,9 @@ internal class DefaultCardCryptoCurrencyFactoryTest { fun `create currencies for single-currency wallet (ETH)`(model: CreateTestModel.SingleWallet) = runTest { // Arrange val userWallet = createSingleWallet() + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) - coEvery { userWalletsListRepository.getSyncStrict(id = userWallet.walletId) } returns userWallet + every { userWalletsListRepository.userWallets } returns userWalletsFlow // Act val actual = factory.create(userWalletId = userWallet.walletId, network = model.network) @@ -142,7 +145,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - userWalletsListRepository.getSyncStrict(id = userWallet.walletId) + userWalletsListRepository.userWallets userWallet.scanResponse.cardTypesResolver.getBlockchain() } @@ -168,8 +171,9 @@ internal class DefaultCardCryptoCurrencyFactoryTest { ) = runTest { // Arrange val userWallet = MockUserWalletFactory.createSingleWalletWithToken() + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) - coEvery { userWalletsListRepository.getSyncStrict(id = userWallet.walletId) } returns userWallet + every { userWalletsListRepository.userWallets } returns userWalletsFlow // Act val actual = factory.create(userWalletId = userWallet.walletId, network = model.network) @@ -186,7 +190,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - userWalletsListRepository.getSyncStrict(id = userWallet.walletId) + userWalletsListRepository.userWallets userWallet.scanResponse.cardTypesResolver.getBlockchain() } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt index 9623d72208..16bb9deefe 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt @@ -14,6 +14,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -118,8 +119,10 @@ class UserTokensSaverTest { val error = ApiResponseError.UnknownException(Exception("API Error")) var onFailSendCalled = false + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + every { accountsFeatureToggles.isFeatureEnabled } returns true - coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns userWallet + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { enricher(userWalletId, response) } returns enrichedResponse coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse @@ -132,6 +135,7 @@ class UserTokensSaverTest { // THEN coVerifyOrder { + userWalletsListRepository.userWallets enricher(userWalletId, response) tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse) } @@ -165,7 +169,9 @@ class UserTokensSaverTest { walletType = WalletType.COLD, ) - coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { enricher(userWalletId, response) } returns enrichedResponse coEvery { tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse) @@ -177,6 +183,7 @@ class UserTokensSaverTest { // THEN coVerifyOrder { enricher(userWalletId, response) + userWalletsListRepository.userWallets tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse) } } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinderTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinderTest.kt index e599ae0254..c1c4e5ceef 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinderTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinderTest.kt @@ -14,6 +14,7 @@ import com.tangem.domain.wallets.models.AppsFlyerConversionData import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Nested @@ -65,7 +66,9 @@ internal class DefaultWalletServerBinderTest { ) val apiResponse = ApiResponse.Success(Unit) - coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { appsFlyerStore.get() } returns conversionData coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse @@ -74,7 +77,7 @@ internal class DefaultWalletServerBinderTest { Truth.assertThat(actual).isEqualTo(apiResponse) coVerifyOrder { - userWalletsListRepository.getSyncOrNull(userWalletId) + userWalletsListRepository.userWallets appsFlyerStore.get() tangemTechApi.createWallet(requestBody) } @@ -82,15 +85,15 @@ internal class DefaultWalletServerBinderTest { @Test fun `bind will skipped if userWalletsListRepository returns null`() = runTest { - coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns null + val userWalletsFlow = MutableStateFlow?>(null) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val actual = binder.bind(userWalletId = userWalletId) Truth.assertThat(actual).isEqualTo(null) - coVerifyOrder { - userWalletsListRepository.getSyncOrNull(userWalletId) - } + coVerifyOrder { userWalletsListRepository.userWallets } coVerify(inverse = true) { appsFlyerStore.get() @@ -107,7 +110,9 @@ internal class DefaultWalletServerBinderTest { ) val apiResponse = ApiResponse.Success(Unit) - coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { appsFlyerStore.get() } returns null coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse @@ -116,7 +121,7 @@ internal class DefaultWalletServerBinderTest { Truth.assertThat(actual).isEqualTo(apiResponse) coVerifyOrder { - userWalletsListRepository.getSyncOrNull(userWalletId) + userWalletsListRepository.userWallets appsFlyerStore.get() tangemTechApi.createWallet(requestBody) } @@ -133,7 +138,9 @@ internal class DefaultWalletServerBinderTest { ) val apiResponse = ApiResponse.Error(ApiResponseError.TimeoutException()) as ApiResponse - coEvery { userWalletsListRepository.getSyncOrNull(userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { appsFlyerStore.get() } returns conversionData coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse @@ -142,7 +149,7 @@ internal class DefaultWalletServerBinderTest { Truth.assertThat(actual).isEqualTo(apiResponse) coVerifyOrder { - userWalletsListRepository.getSyncOrNull(userWalletId) + userWalletsListRepository.userWallets appsFlyerStore.get() tangemTechApi.createWallet(requestBody) } 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 cb16c2ef93..6e97e2c7b5 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 @@ -11,6 +11,7 @@ import com.tangem.datasource.exchangeservice.swap.ExpressUtils.getRefCode import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.catchOn import com.tangem.domain.core.utils.lceContent diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index 2617383978..7725874819 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -18,6 +18,7 @@ import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains import com.tangem.domain.card.common.extensions.supportedBlockchains import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.managetokens.model.AddCustomTokenForm import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.repository.CustomTokensRepository diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index 4786abe9db..98f8097fbc 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -27,6 +27,7 @@ import com.tangem.domain.card.common.TapWorkarounds.isTestCard import com.tangem.domain.card.common.extensions.* import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.managetokens.model.* import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.managetokens.repository.ManageTokensRepository diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index f3a58f68e2..834c80a691 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -21,6 +21,7 @@ import com.tangem.datasource.api.markets.models.response.TokenMarketExchangesRes import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.models.account.DerivationIndex 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 f0546b2d68..8406342ea9 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt @@ -5,6 +5,7 @@ import arrow.core.some import com.tangem.data.common.network.NetworkFactory import com.tangem.data.networks.store.NetworksStatusesStore import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncOrNull import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.networks.multi.MultiNetworkStatusProducer diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt index f677f10935..4b35a6fa71 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt @@ -11,6 +11,7 @@ import com.tangem.data.networks.store.NetworksStatusesStore import com.tangem.data.networks.toSimple import com.tangem.domain.card.configs.GenericCardConfig import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncOrNull import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.networks.multi.MultiNetworkStatusProducer @@ -65,7 +66,9 @@ internal class DefaultMultiNetworkStatusProducerTest { val networksStatusesFlow = flowOf(simpleStatuses) every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow - every { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { networkFactory.create( @@ -94,7 +97,7 @@ internal class DefaultMultiNetworkStatusProducerTest { verifyOrder { networksStatusesStore.get(params.userWalletId) - userWalletsListRepository.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, @@ -129,7 +132,9 @@ internal class DefaultMultiNetworkStatusProducerTest { // region every every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow - every { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { networkFactory.create( networkId = simpleStatuses.first().id, @@ -177,7 +182,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder { - userWalletsListRepository.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, @@ -202,7 +207,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual2).isEqualTo(expected2) verifyOrder { - userWalletsListRepository.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets networkFactory.create( networkId = updatedSimpleStatuses.first().id, derivationPath = updatedSimpleStatuses.first().id.derivationPath, @@ -230,7 +235,9 @@ internal class DefaultMultiNetworkStatusProducerTest { // region every every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow - every { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { networkFactory.create( @@ -263,7 +270,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder { - userWalletsListRepository.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, @@ -312,7 +319,9 @@ internal class DefaultMultiNetworkStatusProducerTest { // region every every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow - every { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { networkFactory.create( networkId = simpleStatuses.first().id, @@ -354,7 +363,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual2.first()).isEqualTo(expected2) verifyOrder { - userWalletsListRepository.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, @@ -382,7 +391,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual.first()).isEqualTo(expected) verify { networksStatusesStore.get(params.userWalletId) } - verify(inverse = true) { userWalletsListRepository.getSyncOrNull(params.userWalletId) } + verify(inverse = true) { userWalletsListRepository.userWallets } } @Test @@ -398,7 +407,9 @@ internal class DefaultMultiNetworkStatusProducerTest { val networksStatusesFlow = flowOf(simpleStatuses) every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow - every { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { networkFactory.create(networkId = any(), any(), any()) } returns null // Act @@ -411,7 +422,7 @@ internal class DefaultMultiNetworkStatusProducerTest { verifyOrder { networksStatusesStore.get(params.userWalletId) - userWalletsListRepository.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, 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 b16adf3821..7b022349ef 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 @@ -18,6 +18,7 @@ import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter 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.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network 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 aa5a415297..6501990863 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,6 +22,8 @@ import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.extensions.canHandleBlockchain 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.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.HotCryptoCurrency @@ -89,8 +91,7 @@ internal class DefaultHotCryptoRepository( .map { it[userWalletId] } .filterNotNull() .map { response -> - val userWallet = userWalletsListRepository.getSyncOrNull(userWalletId) - ?: error("UserWalletId [$userWalletId] not found") + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) HotCryptoCurrencyConverter( userWallet = userWallet, 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 a8c3926c66..c957ac3d12 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 @@ -18,6 +18,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.datasource.local.token.P2PEthPoolVaultsStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncOrNull import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWallet diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt index d37c4d2c87..3290286794 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt @@ -16,12 +16,12 @@ import com.tangem.datasource.local.token.P2PEthPoolVaultsStore import com.tangem.datasource.local.token.StakingYieldsStore import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.test.core.assertEitherLeft import com.tangem.test.core.assertEitherRight import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -62,7 +62,9 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId)) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields @@ -80,7 +82,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsListRepository.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getMultipleYieldBalances(requests) @@ -97,7 +99,9 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val yields = listOf(MockYieldDTOFactory.create(tonId)) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields @@ -112,7 +116,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsListRepository.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId)) @@ -129,13 +133,15 @@ internal class DefaultMultiStakingBalanceFetcherTest { val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false) - coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow // Actual val actual = fetcher.invoke(params) // Assert - coVerifyOrder { userWalletsListRepository.getSyncOrNull(params.userWalletId) } + coVerifyOrder { userWalletsListRepository.userWallets } coVerify(inverse = true) { stakeKitBalancesStore.refresh(userWalletId = any(), stakingIds = any()) @@ -155,13 +161,14 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns null + val userWalletsFlow = MutableStateFlow(null) + coEvery { userWalletsListRepository.userWallets } returns userWalletsFlow // Actual val actual = fetcher.invoke(params) // Assert - coVerifyOrder { userWalletsListRepository.getSyncOrNull(params.userWalletId) } + coVerifyOrder { userWalletsListRepository.userWallets } coVerify(inverse = true) { stakeKitBalancesStore.refresh(userWalletId = any(), stakingIds = any()) @@ -181,7 +188,9 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { stakingYieldsStore.getSyncWithTimeout() } returns null // Actual @@ -189,7 +198,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsListRepository.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets stakeKitBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -210,7 +219,9 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { stakingYieldsStore.getSyncWithTimeout() } returns emptyList() // Actual @@ -218,7 +229,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsListRepository.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -239,7 +250,9 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val yields = listOf( MockYieldDTOFactory.create(tonId).copy(id = null), @@ -252,7 +265,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsListRepository.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -273,7 +286,9 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val yields = listOf(MockYieldDTOFactory.create(StakingID(integrationId = "polygon", address = "0x1"))) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields @@ -283,7 +298,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsListRepository.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -310,7 +325,9 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsListRepository.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId)) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields @@ -328,7 +345,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsListRepository.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getMultipleYieldBalances(requests) @@ -343,8 +360,8 @@ internal class DefaultMultiStakingBalanceFetcherTest { } private companion object { - val userWalletId = UserWalletId("011") val userWallet = MockUserWalletFactory.create() + val userWalletId = userWallet.walletId val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId val solanaId = StakingID( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcher.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcher.kt index 2eb230d05d..642add41ce 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcher.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcher.kt @@ -5,6 +5,7 @@ import arrow.core.right import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.datasource.api.tangemTech.models.account.flattenTokens import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.core.utils.catchOn import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.express.models.ExpressAsset diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt index 8e1b04b9bb..4b563eb468 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt @@ -11,6 +11,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.core.utils.catchOn import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.express.ExpressServiceFetcher diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index c8755050b6..b2ec4a4968 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -17,6 +17,8 @@ import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict +import com.tangem.domain.common.wallets.loadAndGet import com.tangem.domain.core.error.DataError import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.express.ExpressServiceFetcher diff --git a/data/tokens/src/test/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcherTest.kt b/data/tokens/src/test/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcherTest.kt index 3e544d681b..3f95f270ca 100644 --- a/data/tokens/src/test/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcherTest.kt +++ b/data/tokens/src/test/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcherTest.kt @@ -14,6 +14,7 @@ import com.tangem.test.core.assertEither import com.tangem.test.core.assertEitherRight import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -43,8 +44,13 @@ internal class AccountListCryptoCurrenciesFetcherTest { fun `returns failure if wallet is not multi-currency`() = runTest { // Arrange val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - val mockUserWallet = mockk { every { isMultiCurrency } returns false } - every { userWalletsListRepository.getSyncStrict(id = params.userWalletId) } returns mockUserWallet + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns false + } + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow // Act val actual = fetcher(params) @@ -55,7 +61,7 @@ internal class AccountListCryptoCurrenciesFetcherTest { ).left() assertEither(actual, expected) - verify { userWalletsListRepository.getSyncStrict(id = params.userWalletId) } + verify { userWalletsListRepository.userWallets } coVerify(inverse = true) { walletAccountsFetcher.fetch(any()) } } @@ -63,10 +69,15 @@ internal class AccountListCryptoCurrenciesFetcherTest { fun `returns accounts if wallet is multi-currency`() = runTest { // Arrange val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - val mockUserWallet = mockk { every { isMultiCurrency } returns true } + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + } val response = mockk(relaxed = true) - every { userWalletsListRepository.getSyncStrict(id = params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { walletAccountsFetcher.fetch(userWalletId = params.userWalletId) } returns response coEvery { expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = emptySet()) } returns Unit.right() @@ -77,7 +88,7 @@ internal class AccountListCryptoCurrenciesFetcherTest { assertEitherRight(actual) coVerify(ordering = Ordering.SEQUENCE) { - userWalletsListRepository.getSyncStrict(id = params.userWalletId) + userWalletsListRepository.userWallets walletAccountsFetcher.fetch(userWalletId = params.userWalletId) expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = emptySet()) } @@ -87,10 +98,15 @@ internal class AccountListCryptoCurrenciesFetcherTest { fun `returns error if walletAccountsFetcher returns error`() = runTest { // Arrange val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - val mockUserWallet = mockk { every { isMultiCurrency } returns true } + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + } val error = RuntimeException("fetch error") - every { userWalletsListRepository.getSyncStrict(id = params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { walletAccountsFetcher.fetch(userWalletId = params.userWalletId) } throws error // Act @@ -101,7 +117,7 @@ internal class AccountListCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerify(ordering = Ordering.SEQUENCE) { - userWalletsListRepository.getSyncStrict(id = params.userWalletId) + userWalletsListRepository.userWallets walletAccountsFetcher.fetch(userWalletId = params.userWalletId) } } diff --git a/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt index b0a5170a5a..1b649ebbe0 100644 --- a/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt +++ b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt @@ -24,6 +24,7 @@ import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher import com.tangem.test.core.assertEither import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -76,10 +77,13 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) val mockUserWallet = mockk { + every { walletId } returns userWalletId every { isMultiCurrency } returns false } - every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow // Act val actual = fetcher(params) @@ -90,7 +94,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { ).left() assertEither(actual, expected) - verifyOrder { userWalletsListRepository.getSyncStrict(id = params.userWalletId) } + verifyOrder { userWalletsListRepository.userWallets } coVerify(inverse = true) { userTokensResponseStore.getSyncOrNull(any()) } @@ -121,7 +125,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { ), ) - every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns null every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) @@ -145,7 +151,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsListRepository.getSyncStrict(id = params.userWalletId) + userWalletsListRepository.userWallets userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) @@ -169,7 +175,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN), ) - every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns defaultResponse coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { @@ -191,7 +199,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsListRepository.getSyncStrict(id = params.userWalletId) + userWalletsListRepository.userWallets userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) @@ -219,7 +227,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN), ) - every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) @@ -240,7 +250,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsListRepository.getSyncStrict(id = params.userWalletId) + userWalletsListRepository.userWallets tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data) @@ -283,7 +293,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { ), ) - every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null coEvery { @@ -308,7 +320,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsListRepository.getSyncStrict(id = params.userWalletId) + userWalletsListRepository.userWallets tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) @@ -337,7 +349,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { cause = ApiResponseError.TimeoutException(), ) as ApiResponse - every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns defaultResponse coEvery { @@ -359,7 +373,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsListRepository.getSyncStrict(id = params.userWalletId) + userWalletsListRepository.userWallets tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) @@ -407,7 +421,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { ), ) - every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null coEvery { @@ -432,7 +448,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsListRepository.getSyncStrict(id = params.userWalletId) + userWalletsListRepository.userWallets tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) @@ -463,7 +479,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { ), ) as ApiResponse - every { userWalletsListRepository.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns defaultResponse coEvery { @@ -485,7 +503,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsListRepository.getSyncStrict(id = params.userWalletId) + userWalletsListRepository.userWallets tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) userTokensSaver.push(userWalletId = params.userWalletId, response = defaultResponse) 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 8c026a475d..23761f8743 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 @@ -9,6 +9,7 @@ import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource import com.tangem.datasource.local.txhistory.TxHistoryItemsStore import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.wallet.UserWalletId diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index 978d392878..d095101533 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -18,6 +18,7 @@ import com.tangem.datasource.api.visa.VisaApi import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.common.wallets.UserWalletsListRepository +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.models.wallet.requireColdWallet diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt index ce95f24b90..eb91e4a601 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt @@ -9,6 +9,7 @@ import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequ import com.tangem.datasource.api.visa.VisaApi import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.common.wallets.update import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWalletConnectRepository.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWalletConnectRepository.kt index 0aa06feafc..4d908dbb8b 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWalletConnectRepository.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWalletConnectRepository.kt @@ -1,6 +1,7 @@ package com.tangem.data.walletconnect import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.walletconnect.repository.WalletConnectRepository 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 cd3223e10a..d10812bf6b 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 @@ -30,6 +30,7 @@ import com.tangem.data.walletmanager.utils.* import com.tangem.datasource.asset.loader.AssetLoader import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network 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 b6f5329aa5..83af408c47 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 @@ -26,6 +26,8 @@ import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository +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 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 e355e23257..534b16cb36 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 @@ -4,6 +4,7 @@ import arrow.core.getOrElse import com.tangem.common.extensions.ByteArrayKey import com.tangem.crypto.hdWallet.DerivationPath 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 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 0484eb18f6..8d0f7ec5d1 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 @@ -8,6 +8,7 @@ import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.common.network.NetworkFactory import com.tangem.data.wallets.derivations.MissedDerivationsFinder 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 diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt index 08a0df11ed..3f759cd81e 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt @@ -12,6 +12,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test @@ -51,15 +52,21 @@ internal class DefaultDerivationsRepositoryTest { @Test fun `error if userWalletId not found`() = runTest { val currencies = MockCryptoCurrencyFactory(defaultUserWallet).ethereum.let(::listOf) - coEvery { userWalletsListRepository.getSyncStrict(defaultUserWalletId) } throws IllegalStateException() + val userWalletsFlow = MutableStateFlow?>(null) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow runCatching { repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = currencies) } .onSuccess { error("Should throws exception") } - .onFailure { Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) } + .onFailure { + Truth.assertThat(it).isInstanceOf(IllegalArgumentException::class.java) + Truth.assertThat(it).hasMessageThat() + .isEqualTo("Unable to find user wallet with provided ID: $defaultUserWalletId") + } - coVerify(exactly = 1) { userWalletsListRepository.getSyncStrict(defaultUserWalletId) } + coVerify(exactly = 1) { userWalletsListRepository.userWallets } coVerify(inverse = true) { coldDerivationsRepository.derivePublicKeysByNetworks(any(), any()) userWalletsListRepository.saveWithoutLock(any(), any()) @@ -71,7 +78,7 @@ internal class DefaultDerivationsRepositoryTest { repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) coVerify(inverse = true) { - userWalletsListRepository.getSyncStrict(any()) + userWalletsListRepository.userWallets coldDerivationsRepository.derivePublicKeysByNetworks(any(), any()) userWalletsListRepository.saveWithoutLock(any(), any()) } @@ -80,8 +87,10 @@ internal class DefaultDerivationsRepositoryTest { @Test fun `error if coldDerivationsRepository throws exception`() = runTest { val currencies = MockCryptoCurrencyFactory(defaultUserWallet).ethereum.let(::listOf) + val userWalletsFlow = MutableStateFlow(listOf(defaultUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow - coEvery { userWalletsListRepository.getSyncStrict(defaultUserWalletId) } returns defaultUserWallet coEvery { coldDerivationsRepository.derivePublicKeysByNetworks( userWallet = defaultUserWallet, @@ -96,7 +105,7 @@ internal class DefaultDerivationsRepositoryTest { .onFailure { Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) } coVerifyOrder { - userWalletsListRepository.getSyncStrict(defaultUserWalletId) + userWalletsListRepository.userWallets coldDerivationsRepository.derivePublicKeysByNetworks( userWallet = defaultUserWallet, networks = currencies.map(CryptoCurrency.Coin::network), @@ -109,9 +118,11 @@ internal class DefaultDerivationsRepositoryTest { @Test fun `success case`() = runTest { val currencies = MockCryptoCurrencyFactory(defaultUserWallet).ethereum.let(::listOf) + val userWalletsFlow = MutableStateFlow(listOf(defaultUserWallet)) val updatedWallet = defaultUserWallet.copy(cardsInWallet = setOf("AC01")) - coEvery { userWalletsListRepository.getSyncStrict(defaultUserWalletId) } returns defaultUserWallet + every { userWalletsListRepository.userWallets } returns userWalletsFlow + coEvery { coldDerivationsRepository.derivePublicKeysByNetworks( userWallet = defaultUserWallet, @@ -125,7 +136,7 @@ internal class DefaultDerivationsRepositoryTest { repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = currencies) coVerifyOrder { - userWalletsListRepository.getSyncStrict(defaultUserWalletId) + userWalletsListRepository.userWallets coldDerivationsRepository.derivePublicKeysByNetworks( userWallet = defaultUserWallet, networks = currencies.map(CryptoCurrency.Coin::network), diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt index f374e1867d..2a0bfba4d0 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt @@ -5,6 +5,7 @@ import arrow.core.getOrElse import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.loadAndGet import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt index 9b9f38a4c6..750b29926b 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt @@ -10,7 +10,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency import io.mockk.* -import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOf @@ -25,7 +25,7 @@ import org.junit.jupiter.api.TestInstance class IsAccountsModeEnabledUseCaseTest { private val accountsCRUDRepository: AccountsCRUDRepository = mockk() - private val userWalletsListRepository: UserWalletsListRepository = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) private val featureToggles: AccountsFeatureToggles = mockk() private val useCase = IsAccountsModeEnabledUseCase( @@ -55,27 +55,10 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isFalse() verify(exactly = 1) { featureToggles.isFeatureEnabled } - verify(inverse = true) { userWalletsListRepository.loadAndGet() } - } - - @Test - fun `returns false when loadAndGet emits empty flow`() = runTest { - // Arrange - every { featureToggles.isFeatureEnabled } returns true - every { userWalletsListRepository.loadAndGet() } returns emptyFlow() - - // Act - val actual = useCase.invoke().firstOrNull() - - // Assert - Truth.assertThat(actual).isFalse() - - verifyOrder { - featureToggles.isFeatureEnabled - userWalletsListRepository.loadAndGet() + coVerify(inverse = true) { + userWalletsListRepository.load() + userWalletsListRepository.userWallets } - - verify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCount(any()) } } @Test @@ -84,7 +67,7 @@ class IsAccountsModeEnabledUseCaseTest { val wallet = createUserWallet(isMultiCurrency = false) every { featureToggles.isFeatureEnabled } returns true - every { userWalletsListRepository.loadAndGet() } returns flowOf(listOf(wallet)) + every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet)) // Act val actual = useCase.invoke().first() @@ -92,9 +75,10 @@ class IsAccountsModeEnabledUseCaseTest { // Assert Truth.assertThat(actual).isFalse() - verifyOrder { + coVerifyOrder { featureToggles.isFeatureEnabled - userWalletsListRepository.loadAndGet() + userWalletsListRepository.load() + userWalletsListRepository.userWallets } verify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCount(any()) } @@ -106,7 +90,7 @@ class IsAccountsModeEnabledUseCaseTest { val wallet = createUserWallet(isMultiCurrency = true) every { featureToggles.isFeatureEnabled } returns true - every { userWalletsListRepository.loadAndGet() } returns flowOf(listOf(wallet)) + every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet)) every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } returns flowOf(2.some()) // Act @@ -115,9 +99,10 @@ class IsAccountsModeEnabledUseCaseTest { // Assert Truth.assertThat(actual).isTrue() - verifyOrder { + coVerifyOrder { featureToggles.isFeatureEnabled - userWalletsListRepository.loadAndGet() + userWalletsListRepository.load() + userWalletsListRepository.userWallets accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } } @@ -128,7 +113,7 @@ class IsAccountsModeEnabledUseCaseTest { val wallet = createUserWallet(isMultiCurrency = true) every { featureToggles.isFeatureEnabled } returns true - every { userWalletsListRepository.loadAndGet() } returns flowOf(listOf(wallet)) + every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet)) every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } returns flowOf(none()) // Act @@ -137,9 +122,10 @@ class IsAccountsModeEnabledUseCaseTest { // Assert Truth.assertThat(actual).isFalse() - verifyOrder { + coVerifyOrder { featureToggles.isFeatureEnabled - userWalletsListRepository.loadAndGet() + userWalletsListRepository.load() + userWalletsListRepository.userWallets accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } } @@ -151,7 +137,7 @@ class IsAccountsModeEnabledUseCaseTest { val wallet2 = createUserWallet(isMultiCurrency = true) every { featureToggles.isFeatureEnabled } returns true - every { userWalletsListRepository.loadAndGet() } returns flowOf(listOf(wallet1, wallet2)) + every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet1, wallet2)) every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet2.walletId) } returns flowOf(2.some()) // Act @@ -160,9 +146,10 @@ class IsAccountsModeEnabledUseCaseTest { // Assert Truth.assertThat(actual).isTrue() - verifyOrder { + coVerifyOrder { featureToggles.isFeatureEnabled - userWalletsListRepository.loadAndGet() + userWalletsListRepository.load() + userWalletsListRepository.userWallets accountsCRUDRepository.getTotalActiveAccountsCount(wallet2.walletId) } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducer.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducer.kt index e5c1ecf871..edecc9f2c4 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducer.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducer.kt @@ -5,6 +5,7 @@ import arrow.core.some import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.loadAndGet import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted 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 db4baa88fa..be5c3f927d 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 @@ -9,6 +9,7 @@ import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.supplier.SingleAccountListSupplier 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.core.utils.lceContent import com.tangem.domain.core.utils.lceLoading diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetCryptoCurrencyActionsUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetCryptoCurrencyActionsUseCaseV2.kt index 9ec1d14e63..c0148890fb 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetCryptoCurrencyActionsUseCaseV2.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetCryptoCurrencyActionsUseCaseV2.kt @@ -4,6 +4,7 @@ import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.utils.AccountCryptoCurrencyStatusFinder import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt index 48ecd270a8..67ed61d8e6 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt @@ -9,7 +9,10 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.test.core.getEmittedValues import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.* +import io.mockk.clearMocks +import io.mockk.coVerifySequence +import io.mockk.every +import io.mockk.mockk import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -21,7 +24,7 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultMultiAccountStatusListProducerTest { - private val userWalletsListRepository: UserWalletsListRepository = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier = mockk() private val dispatchers = TestingCoroutineDispatcherProvider() private val flowProducerTools: FlowProducerTools = mockk() @@ -55,7 +58,7 @@ class DefaultMultiAccountStatusListProducerTest { val wallets = listOf(userWallet1, userWallet2) val walletsFlow = MutableStateFlow(wallets) - every { userWalletsListRepository.loadAndGet() } returns walletsFlow + every { userWalletsListRepository.userWallets } returns walletsFlow val accountStatusList1 = mockk() val accountStatusList2 = mockk() @@ -78,8 +81,9 @@ class DefaultMultiAccountStatusListProducerTest { val expected = listOf(accountStatusList1, accountStatusList2) Truth.assertThat(actual).containsExactly(expected) - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsListRepository.loadAndGet() + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets singleAccountStatusListSupplier( params = SingleAccountStatusListProducer.Params(userWalletId1), ) @@ -93,7 +97,7 @@ class DefaultMultiAccountStatusListProducerTest { fun `produce returns empty flow if userWallets is empty list`() = runTest { // Arrange val walletsFlow = MutableStateFlow>(emptyList()) - every { userWalletsListRepository.loadAndGet() } returns walletsFlow + every { userWalletsListRepository.userWallets } returns walletsFlow // Act val actual = producer.produce().let(::getEmittedValues) @@ -101,16 +105,17 @@ class DefaultMultiAccountStatusListProducerTest { // Assert Truth.assertThat(actual).isEmpty() - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsListRepository.loadAndGet() + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets } } @Test fun `produce returns empty flow if userWallets is null`() = runTest { // Arrange - val walletsFlow = flowOf>(emptyList()) - every { userWalletsListRepository.loadAndGet() } returns walletsFlow + val walletsFlow = MutableStateFlow>(emptyList()) + every { userWalletsListRepository.userWallets } returns walletsFlow // Act val actual = producer.produce().let(::getEmittedValues) @@ -118,8 +123,9 @@ class DefaultMultiAccountStatusListProducerTest { // Assert Truth.assertThat(actual).isEmpty() - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsListRepository.loadAndGet() + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets } } @@ -130,7 +136,7 @@ class DefaultMultiAccountStatusListProducerTest { val userWallet3 = mockk { every { walletId } returns userWalletId3 } val walletsFlow = MutableStateFlow(listOf(userWallet1, userWallet2)) - every { userWalletsListRepository.loadAndGet() } returns walletsFlow + every { userWalletsListRepository.userWallets } returns walletsFlow val accountStatusList1 = mockk() val accountStatusList2 = mockk() @@ -169,8 +175,9 @@ class DefaultMultiAccountStatusListProducerTest { val expected2 = listOf(accountStatusList1, accountStatusList2, accountStatusList3) Truth.assertThat(actual2).containsExactly(expected2) - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsListRepository.loadAndGet() + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets singleAccountStatusListSupplier( params = SingleAccountStatusListProducer.Params(userWalletId1), ) @@ -186,7 +193,8 @@ class DefaultMultiAccountStatusListProducerTest { singleAccountStatusListSupplier( params = SingleAccountStatusListProducer.Params(userWalletId3), ) - userWalletsListRepository.loadAndGet() + userWalletsListRepository.load() + userWalletsListRepository.userWallets singleAccountStatusListSupplier( params = SingleAccountStatusListProducer.Params(userWalletId1), ) diff --git a/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt index fa1a12c054..c9f9aecb00 100644 --- a/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt +++ b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt @@ -5,7 +5,6 @@ import com.tangem.domain.common.wallets.error.* import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow /** @@ -31,21 +30,12 @@ interface UserWalletsListRepository { */ val selectedUserWallet: StateFlow - /** Get user wallet by [id] */ - fun getSyncOrNull(id: UserWalletId): UserWallet? - - /** Get user wallet by [id] */ - fun getSyncStrict(id: UserWalletId): UserWallet - /** * Loads user wallets list and selected wallet. * If the list is already loaded, it does nothing. */ suspend fun load() - /** Loads user wallets list and selected wallet and returns a flow of the list */ - fun loadAndGet(): Flow> - /** * Gets and if necessary loads user wallets list and selected wallet. */ diff --git a/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepositoryExt.kt b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepositoryExt.kt index d1474f7fd8..c5ee21b86a 100644 --- a/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepositoryExt.kt +++ b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepositoryExt.kt @@ -5,6 +5,8 @@ import arrow.core.raise.either import com.tangem.domain.common.wallets.error.SaveWalletError import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow /** * Update user wallet by [userWalletId] and return updated wallet. @@ -29,4 +31,22 @@ suspend fun UserWalletsListRepository.update( .bind() updatedUserWallet +} + +/** Get user wallet by [id] */ +fun UserWalletsListRepository.getSyncOrNull(id: UserWalletId): UserWallet? { + return userWallets.value?.find { it.walletId == id } +} + +/** Get user wallet by [id] or throw an exception if it is not found */ +fun UserWalletsListRepository.getSyncStrict(id: UserWalletId): UserWallet { + return requireNotNull(getSyncOrNull(id)) { "Unable to find user wallet with provided ID: $id" } +} + +/** Loads user wallets list and selected wallet and returns a flow of the list */ +fun UserWalletsListRepository.loadAndGet(): Flow> = flow { + load() + userWallets.collect { + emit(requireNotNull(it)) + } } \ No newline at end of file diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt index 125322a147..5e766048e2 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -9,6 +9,7 @@ import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.StartReferralBody 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.wallet.UserWalletId From fdc22278e68d7d3f5bb936b8e0c7a4fad39b5e05 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 12:00:52 +0400 Subject: [PATCH 044/106] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 3 ++ .../api/tangemTech/TangemTechApi.kt | 2 +- .../tangemTech/models/TransactionEventBody.kt | 1 + .../DefaultPushNotificationsRepository.kt | 9 +++++ .../DefaultTransactionRepository.kt | 7 +++- .../repository/PushNotificationsRepository.kt | 3 ++ domain/transaction/build.gradle.kts | 1 + .../transaction/TransactionRepository.kt | 2 +- .../usecase/SendTransactionUseCase.kt | 40 ++++++++++++++++--- 9 files changed, 60 insertions(+), 8 deletions(-) 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 55dab8c3b9..52ecf3a396 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 @@ -5,6 +5,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier +import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase @@ -56,6 +57,7 @@ internal object TransactionDomainModule { singleNetworkStatusFetcher: SingleNetworkStatusFetcher, tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory, dispatchers: CoroutineDispatcherProvider, + pushNotificationsRepository: PushNotificationsRepository, ): SendTransactionUseCase { return SendTransactionUseCase( demoConfig = DemoConfig, @@ -65,6 +67,7 @@ internal object TransactionDomainModule { singleNetworkStatusFetcher = singleNetworkStatusFetcher, parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.io), getHotWalletSigner = tangemHotWalletSignerFactory::create, + pushNotificationsRepository = pushNotificationsRepository, ) } 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 cf0d66f054..aa8748eb2c 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 @@ -203,7 +203,7 @@ interface TangemTechApi { * Used when yield operations generate intermediate transactions * that should not trigger notifications. */ - @POST("v1/transaction-events") + @POST("v2/transaction-events") suspend fun transactionEvents(@Body name: TransactionEventBody): ApiResponse // region Earn diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/TransactionEventBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/TransactionEventBody.kt index 806b775af1..853a2ce043 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/TransactionEventBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/TransactionEventBody.kt @@ -7,6 +7,7 @@ import com.squareup.moshi.JsonClass data class TransactionEventBody( @Json(name = "transactionId") val transactionId: String, @Json(name = "operationType") val operationType: OperationType, + @Json(name = "userAddress") val userAddress: String? = null, ) @JsonClass(generateAdapter = false) diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt index e98f62d817..34a6666a6c 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt @@ -15,10 +15,14 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.models.NotificationsError import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.domain.notifications.models.NotificationsEligibleNetwork +import com.tangem.datasource.local.preferences.utils.getObjectMap +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.info.AppInfoProvider import kotlinx.coroutines.withContext @@ -96,4 +100,9 @@ internal class DefaultPushNotificationsRepository @Inject constructor( NotificationsEligibleNetworkConverter.convert(it) } } + + override suspend fun isNotificationsEnabled(userWalletId: UserWalletId): Boolean = + appPreferencesStore.getObjectMap(PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY) + .map { it[userWalletId.stringValue] == true } + .firstOrNull() == true } \ 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 f7c4ffff50..06b461cbd4 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 @@ -422,7 +422,11 @@ internal class DefaultTransactionRepository( preparer.prepareAndSignMultiple(transactionData, signer) } - override suspend fun sendTransactionHash(hash: String, transactionType: EventTransactionTypeDto) { + override suspend fun sendTransactionHash( + hash: String, + transactionType: EventTransactionTypeDto, + userAddress: String?, + ) { runCatching(dispatchers.io) { val operationType = when (transactionType) { EventTransactionTypeDto.DEPOSIT -> OperationType.YIELD_DEPOSIT @@ -432,6 +436,7 @@ internal class DefaultTransactionRepository( val body = TransactionEventBody( operationType = operationType, transactionId = hash, + userAddress = userAddress, ) val response = tangemTechApi.transactionEvents(body) response.fold( diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt index 53f7b08f65..e6024ff337 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt @@ -1,6 +1,7 @@ package com.tangem.domain.notifications.repository import arrow.core.Either +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.models.NotificationsEligibleNetwork import com.tangem.domain.notifications.models.NotificationsError @@ -20,4 +21,6 @@ interface PushNotificationsRepository { @Throws suspend fun getEligibleNetworks(): List + + suspend fun isNotificationsEnabled(userWalletId: UserWalletId): Boolean } \ No newline at end of file diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index 8488e0c17b..9bf72aeddb 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(projects.domain.transaction.models) implementation(projects.domain.demo) implementation(projects.domain.card) + implementation(projects.domain.notifications) api(projects.domain.networks) testRuntimeOnly(deps.test.junit5.engine) 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 efa2d59016..74958179f4 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 @@ -125,5 +125,5 @@ interface TransactionRepository { network: Network, ): com.tangem.blockchain.extensions.Result> - suspend fun sendTransactionHash(hash: String, transactionType: EventTransactionTypeDto) + suspend fun sendTransactionHash(hash: String, transactionType: EventTransactionTypeDto, userAddress: String?) } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 17b9fc38cc..05bc7a7445 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -25,6 +25,7 @@ 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.networks.single.SingleNetworkStatusFetcher +import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.error.parseWrappedError @@ -44,6 +45,7 @@ class SendTransactionUseCase( private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, private val parallelUpdatingScope: CoroutineScope, private val getHotWalletSigner: (UserWallet.Hot) -> TransactionSigner, + private val pushNotificationsRepository: PushNotificationsRepository, ) { suspend operator fun invoke( txsData: List, @@ -112,7 +114,11 @@ class SendTransactionUseCase( } .fold( ifRight = { result -> - processSentTransactionsHashes(txsData, result.hashes) + processSentTransactionsHashes( + userWallet = userWallet, + transactions = txsData, + hashes = result.hashes, + ) result.hashes.right() }, ifLeft = { it.left() }, @@ -128,11 +134,19 @@ class SendTransactionUseCase( .map { it.first() } } - private fun processSentTransactionsHashes(transactions: List, hashes: List) { + private fun processSentTransactionsHashes( + userWallet: UserWallet, + transactions: List, + hashes: List, + ) { parallelUpdatingScope.launch { withContext(NonCancellable) { transactions.forEachIndexed { ind, tx -> - sendHashToBackendIfNeeded(tx, hashes[ind]) + sendHashToBackendIfNeeded( + userWallet = userWallet, + transaction = tx, + txHash = hashes[ind], + ) } } } @@ -141,7 +155,11 @@ class SendTransactionUseCase( /** * Sends tx hash to backend for specific transaction types */ - private suspend fun sendHashToBackendIfNeeded(transaction: TransactionData, txHash: String) { + private suspend fun sendHashToBackendIfNeeded( + userWallet: UserWallet, + transaction: TransactionData, + txHash: String, + ) { (transaction as? TransactionData.Uncompiled)?.let { tx -> val extras = tx.extras when (extras) { @@ -152,7 +170,19 @@ class SendTransactionUseCase( is EthereumYieldSupplyExitCallData -> EventTransactionTypeDto.WITHDRAW else -> return } - transactionRepository.sendTransactionHash(txHash, txType) + val isPushNotificationEnabled = pushNotificationsRepository.isNotificationsEnabled( + userWallet.walletId, + ) + val userAddress = if (isPushNotificationEnabled) { + transaction.sourceAddress + } else { + null + } + transactionRepository.sendTransactionHash( + hash = txHash, + transactionType = txType, + userAddress = userAddress, + ) } } } From 207dbeed3c679082ab5e6e9ab771b9561660b050 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 11:01:01 +0300 Subject: [PATCH 045/106] Updated on 2026-08-14 --- .../market/details/portfolio/add/impl/model/AddTokenModel.kt | 2 +- .../com/tangem/features/managetokens/model/ManageTokensModel.kt | 2 +- .../features/managetokens/model/OnboardingManageTokensModel.kt | 2 +- .../managetokens/utils/list/ManageTokensUseCasesFacade.kt | 2 +- .../features/markets/portfolio/add/impl/model/AddTokenModel.kt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt index d616bf53be..cd16df3f04 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt @@ -52,7 +52,7 @@ internal class AddTokenModel @Inject constructor( flow = params.selectedNetwork.distinctUntilChanged(), flow2 = params.selectedPortfolio.distinctUntilChanged(), transform = { selectedNetwork, selectedPortfolio -> - addTokenJob.cancel() + addTokenJob.join() val isTangemIconVisible = needColdWalletInteraction(selectedNetwork, selectedPortfolio) uiBuilder.updateContent( selectedPortfolio = selectedPortfolio, 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 74ee2da6c5..7c5611d12e 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 @@ -298,7 +298,7 @@ internal class ManageTokensModel @Inject constructor( val networks = currenciesToAdd.values .flatten() .toSet() - .associate { it.backendId to null } + .associate { network -> network.backendId to network.derivationPath.value } val needToInteractWithColdWallet = useCasesFacade.needColdWalletInteraction(networks) state.update { state -> 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 50267c156a..e2aaa97cca 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 @@ -222,7 +222,7 @@ internal class OnboardingManageTokensModel @Inject constructor( val network = currenciesToAdd.values .flatten() .toSet() - .associate { it.backendId to null } + .associate { network -> network.backendId to network.derivationPath.value } val showTangemIcon = useCasesFacade.needColdWalletInteraction(network = network) state.update { state -> state.copy( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt index 7bf8e67db0..e692352aad 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt @@ -124,7 +124,7 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( } } - suspend fun needColdWalletInteraction(network: Map): Boolean = when (mode) { + suspend fun needColdWalletInteraction(network: Map): Boolean = when (mode) { is ManageTokensMode.Account -> coldWalletAndHasMissedDerivationsUseCase.invoke( userWalletId = mode.accountId.userWalletId, networksWithDerivationPath = network, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt index 7f8214d8fa..edc27eb65f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt @@ -52,7 +52,7 @@ internal class AddTokenModel @Inject constructor( flow = params.selectedNetwork.distinctUntilChanged(), flow2 = params.selectedPortfolio.distinctUntilChanged(), transform = { selectedNetwork, selectedPortfolio -> - addTokenJob.cancel() + addTokenJob.join() val isTangemIconVisible = needColdWalletInteraction(selectedNetwork, selectedPortfolio) uiBuilder.updateContent( selectedPortfolio = selectedPortfolio, From 0ae9e75e903f429d8bb8c025c16be66c93e1cdf8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 13:01:17 +0500 Subject: [PATCH 046/106] Updated on 2026-08-14 --- .../converter/CryptoPortfolioConverter.kt | 4 +-- .../producer/WalletAccountListFlowFactory.kt | 4 +-- .../WalletAccountListFlowFactoryTest.kt | 4 +-- .../domain/account/models/AccountList.kt | 2 +- .../usecase/AddCryptoPortfolioUseCase.kt | 2 +- .../usecase/ApplyTokenListSortingUseCaseV2.kt | 19 ++++------- .../usecase/ManageCryptoCurrenciesUseCase.kt | 4 +-- .../usecase/RecoverCryptoPortfolioUseCase.kt | 2 +- .../ApplyTokenListSortingUseCaseTest.kt | 32 ++++++++++--------- .../GetAccountCurrencyByAddressUseCaseTest.kt | 2 +- .../GetAccountCurrencyStatusUseCaseTest.kt | 8 ++--- .../RecoverCryptoPortfolioUseCaseTest.kt | 2 +- .../tangem/domain/models/account/Account.kt | 10 +++--- .../domain/models/account/AccountTest.kt | 16 +++++----- .../viewmodel/TesterAccountsViewModel.kt | 2 +- .../java/com/tangem/test/mock/MockAccounts.kt | 2 +- 16 files changed, 55 insertions(+), 60 deletions(-) diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt index daf35d8a49..fdab8240c3 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt @@ -37,9 +37,9 @@ internal class CryptoPortfolioConverter @AssistedInject constructor( tokens = tokens, userWallet = userWallet, accountIndex = value.derivationIndex.toDerivationIndex(), - ).toSet() + ) } else { - emptySet() + emptyList() }, ) } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt index a53f2c2e6d..2fe0b92a3f 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt @@ -55,9 +55,9 @@ internal class WalletAccountListFlowFactory @Inject constructor( val isSingleWalletWithToken = userWallet.requireColdWallet().cardTypesResolver.isSingleWalletWithToken() val currencies = if (isSingleWalletWithToken) { - cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = userWallet).toSet() + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = userWallet) } else { - setOf(cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet = userWallet)) + listOf(cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet = userWallet)) } return AccountList.empty(userWalletId = userWallet.walletId, cryptoCurrencies = currencies) diff --git a/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt index e0abc97e2a..bcda235987 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt @@ -119,7 +119,7 @@ class WalletAccountListFlowFactoryTest { val actual = factory.create(userWallet.walletId).let(::getEmittedValues) // Assert - val expected = AccountList.empty(userWalletId = userWallet.walletId, cryptoCurrencies = setOf(currency)) + val expected = AccountList.empty(userWalletId = userWallet.walletId, cryptoCurrencies = listOf(currency)) Truth.assertThat(actual).containsExactly(expected) coVerifySequence { @@ -144,7 +144,7 @@ class WalletAccountListFlowFactoryTest { every { userWalletsListRepository.userWallets } returns userWalletsFlow - val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() + val currencies = cryptoCurrencyFactory.ethereumAndStellar every { cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = nodl) } returns currencies.toList() 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 cb0f812e98..c413c7e46f 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 @@ -235,7 +235,7 @@ data class AccountList private constructor( */ fun empty( userWalletId: UserWalletId, - cryptoCurrencies: Set = emptySet(), + cryptoCurrencies: List = emptyList(), sortType: TokensSortType = TokensSortType.NONE, groupType: TokensGroupType = TokensGroupType.NONE, ): AccountList { diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt index cf7992e6ab..aa598d03d9 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt @@ -81,7 +81,7 @@ class AddCryptoPortfolioUseCase( accountName = accountName, icon = icon, derivationIndex = derivationIndex, - cryptoCurrencies = emptySet(), + cryptoCurrencies = emptyList(), ) } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt index f3a5fdeec0..6dc53cca47 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt @@ -14,8 +14,6 @@ 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 kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import timber.log.Timber @@ -137,7 +135,6 @@ class ApplyTokenListSortingUseCaseV2( errors[account.accountId] = it return@map account } - .toSet() account.copy(cryptoCurrencies = accountCurrencies) } @@ -171,17 +168,13 @@ class ApplyTokenListSortingUseCaseV2( private suspend fun Raise.applySorting(accountList: AccountList) { coroutineScope { - val results = awaitAll( - async { - Either.catch { accountsCRUDRepository.saveAccountsLocally(accountList) } - }, - async { - Either.catch { accountsCRUDRepository.syncTokens(accountList.userWalletId) } - }, - ) + val results = Either.catch { + accountsCRUDRepository.saveAccountsLocally(accountList) + accountsCRUDRepository.syncTokens(accountList.userWalletId) + } - ensure(results.none { it.isLeft() }) { - val message = results.mapNotNull { it.leftOrNull() }.joinToString() + ensure(results.isRight()) { + val message = results.leftOrNull() raise(TokenListSortingError.DataError(IllegalStateException(message))) } } 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 dd64e3d0cc..d41defe9be 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 @@ -86,7 +86,7 @@ class ManageCryptoCurrenciesUseCase( } saveAccount( - account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet()), + account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total), ) derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) @@ -126,7 +126,7 @@ class ManageCryptoCurrenciesUseCase( val modifiedCurrencyList = accountStatus.tokenList.flattenCurrencies() .modify(add = listOf(tokenToAdd)) - saveAccount(account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet())) + saveAccount(account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total)) parallelUpdatingScope.launch { syncTokens(userWalletId, modifiedCurrencyList) diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt index c972f97b80..36a47dc929 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt @@ -100,7 +100,7 @@ class RecoverCryptoPortfolioUseCase( accountName = this.name, icon = this.icon, derivationIndex = this.derivationIndex, - cryptoCurrencies = emptySet(), + cryptoCurrencies = emptyList(), ) } diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt index c838e40466..af71ec2c02 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt @@ -65,7 +65,7 @@ internal class ApplyTokenListSortingUseCaseTest { @Test fun `when getAccountListSync throws exception then error should be received`() = runTest { // Arrange - val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = emptySet()) + val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = emptyList()) val exception = IllegalStateException("No internet connection") coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } throws exception @@ -92,7 +92,7 @@ internal class ApplyTokenListSortingUseCaseTest { @Test fun `when saveAccountsLocally throws exception then error should be received`() = runTest { // Arrange - val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = emptySet()) + val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = emptyList()) coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() @@ -114,14 +114,16 @@ internal class ApplyTokenListSortingUseCaseTest { coVerifyOrder { accountsCRUDRepository.getAccountListSync(userWalletId) accountsCRUDRepository.saveAccountsLocally(accountList) - accountsCRUDRepository.syncTokens(userWalletId = userWalletId) + } + coVerifyOrder(inverse = true) { + accountsCRUDRepository.syncTokens(userWalletId = any()) } } @Test fun `when syncTokens throws exception then error should be received`() = runTest { // Arrange - val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = emptySet()) + val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = emptyList()) coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() @@ -156,13 +158,13 @@ internal class ApplyTokenListSortingUseCaseTest { val accountList = AccountList.empty( userWalletId = userWalletId, - cryptoCurrencies = setOf(token1, token2, token3), + cryptoCurrencies = listOf(token1, token2, token3), sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ) coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() - val sortedTokens = setOf(token2, token3, token1) + val sortedTokens = listOf(token2, token3, token1) val updatedAccountList = AccountList.empty( userWalletId = userWalletId, cryptoCurrencies = sortedTokens, @@ -197,13 +199,13 @@ internal class ApplyTokenListSortingUseCaseTest { val accountList = AccountList.empty( userWalletId = userWalletId, - cryptoCurrencies = setOf(token1, token2, token3), + cryptoCurrencies = listOf(token1, token2, token3), sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ) coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() - val sortedTokens = setOf(token2, token3, token1) + val sortedTokens = listOf(token2, token3, token1) val updatedAccountList = AccountList.empty( userWalletId = userWalletId, cryptoCurrencies = sortedTokens, @@ -238,13 +240,13 @@ internal class ApplyTokenListSortingUseCaseTest { val accountList = AccountList.empty( userWalletId = userWalletId, - cryptoCurrencies = setOf(token1, token2, token3), + cryptoCurrencies = listOf(token1, token2, token3), sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ) coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() - val sortedTokens = setOf(token2, token3, token1) + val sortedTokens = listOf(token2, token3, token1) val updatedAccountList = AccountList.empty( userWalletId = userWalletId, cryptoCurrencies = sortedTokens, @@ -279,13 +281,13 @@ internal class ApplyTokenListSortingUseCaseTest { val accountList = AccountList.empty( userWalletId = userWalletId, - cryptoCurrencies = setOf(token1, token2, token3), + cryptoCurrencies = listOf(token1, token2, token3), sortType = TokensSortType.BALANCE, groupType = TokensGroupType.NETWORK, ) coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() - val sortedTokens = setOf(token2, token3, token1) + val sortedTokens = listOf(token2, token3, token1) val updatedAccountList = AccountList.empty( userWalletId = userWalletId, cryptoCurrencies = sortedTokens, @@ -323,13 +325,13 @@ internal class ApplyTokenListSortingUseCaseTest { name = "Account 1", icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), derivationIndex = 1, - cryptoCurrencies = setOf(token1, token2, token3), + cryptoCurrencies = listOf(token1, token2, token3), ) .getOrNull()!! val accountList = (AccountList.empty( userWalletId = userWalletId, - cryptoCurrencies = setOf(token1, token2, token3), + cryptoCurrencies = listOf(token1, token2, token3), sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ) + customAccount).getOrNull()!! @@ -344,7 +346,7 @@ internal class ApplyTokenListSortingUseCaseTest { val updatedAccountList = AccountList( userWalletId = userWalletId, accounts = listOf( - accountList.mainAccount.copy(cryptoCurrencies = setOf(token3, token2, token1)), + accountList.mainAccount.copy(cryptoCurrencies = listOf(token3, token2, token1)), customAccount, // unchanged due to error ), totalAccounts = accountList.totalAccounts, diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt index 596ebd9242..9471a7d81f 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt @@ -252,7 +252,7 @@ class GetAccountCurrencyByAddressUseCaseTest { network = currency.network, value = NetworkStatus.Unreachable(address = validNetworkAddress), ) - val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = setOf(currency)) + val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = listOf(currency)) every { userWalletsListRepository.userWallets.value } returns listOf(multiUserWallet) coEvery { diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt index 3fc7c456cd..5c35e33427 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt @@ -103,7 +103,7 @@ class GetAccountCurrencyStatusUseCaseTest { val account = mockk(relaxed = true) { every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!! - every { this@mockk.cryptoCurrencies } returns setOf(currency) + every { this@mockk.cryptoCurrencies } returns listOf(currency) } val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) val accountStatus = AccountStatus.CryptoPortfolio( @@ -140,7 +140,7 @@ class GetAccountCurrencyStatusUseCaseTest { fun `invokeSync returns Some if network is null`() = runTest { // Arrange val account = mockk(relaxed = true) { - every { this@mockk.cryptoCurrencies } returns setOf(currency) + every { this@mockk.cryptoCurrencies } returns listOf(currency) } val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) val accountStatus = AccountStatus.CryptoPortfolio( @@ -223,7 +223,7 @@ class GetAccountCurrencyStatusUseCaseTest { val account = mockk(relaxed = true) { every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!! - every { this@mockk.cryptoCurrencies } returns setOf(currency) + every { this@mockk.cryptoCurrencies } returns listOf(currency) } val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) val accountStatus = AccountStatus.CryptoPortfolio( @@ -257,7 +257,7 @@ class GetAccountCurrencyStatusUseCaseTest { fun `invoke returns data if network is null`() = runTest { // Arrange val account = mockk(relaxed = true) { - every { this@mockk.cryptoCurrencies } returns setOf(currency) + every { this@mockk.cryptoCurrencies } returns listOf(currency) } val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) val accountStatus = AccountStatus.CryptoPortfolio( diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCaseTest.kt index 8bf3eb7e56..e55d32647c 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCaseTest.kt @@ -266,7 +266,7 @@ class RecoverCryptoPortfolioUseCaseTest { accountName = AccountName(name).getOrNull()!!, icon = icon, derivationIndex = derivationIndex, - cryptoCurrencies = emptySet(), + cryptoCurrencies = emptyList(), ) } 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 017f431065..a0442452ef 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 @@ -42,7 +42,7 @@ sealed interface Account { override val accountName: AccountName, val icon: CryptoPortfolioIcon, val derivationIndex: DerivationIndex, - val cryptoCurrencies: Set, + val cryptoCurrencies: List, ) : Account { /** Indicates if the account is the main account */ @@ -60,7 +60,7 @@ sealed interface Account { fun copy( accountName: AccountName = this.accountName, icon: CryptoPortfolioIcon = this.icon, - cryptoCurrencies: Set = this.cryptoCurrencies, + cryptoCurrencies: List = this.cryptoCurrencies, ): CryptoPortfolio { return CryptoPortfolio( accountId = this.accountId, @@ -102,7 +102,7 @@ sealed interface Account { name: String, icon: CryptoPortfolioIcon, derivationIndex: Int, - cryptoCurrencies: Set = emptySet(), + cryptoCurrencies: List = emptyList(), ): Either { return either { val accountName = AccountName(value = name).getOrElse { @@ -138,7 +138,7 @@ sealed interface Account { accountName: AccountName, icon: CryptoPortfolioIcon, derivationIndex: DerivationIndex, - cryptoCurrencies: Set = emptySet(), + cryptoCurrencies: List = emptyList(), ): CryptoPortfolio { return CryptoPortfolio( accountId = accountId, @@ -157,7 +157,7 @@ sealed interface Account { */ fun createMainAccount( userWalletId: UserWalletId, - cryptoCurrencies: Set = emptySet(), + cryptoCurrencies: List = emptyList(), ): CryptoPortfolio { val derivationIndex = DerivationIndex.Main diff --git a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt index 9578f81fb1..0695ba92c3 100644 --- a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt @@ -50,8 +50,8 @@ class AccountTest { @Test fun `CryptoPortfolio tokensCount`() { // Arrange - val emptyCurrencies = emptySet() - val filledCurrencies = setOf(mockk()) + val emptyCurrencies = emptyList() + val filledCurrencies = listOf(mockk()) // Act val actual1 = createCryptoPortfolioStub(currencies = emptyCurrencies) @@ -68,8 +68,8 @@ class AccountTest { @Test fun `CryptoPortfolio networksCount`() { // Arrange - val emptyCurrencies = emptySet() - val filledCurrencies = setOf( + val emptyCurrencies = emptyList() + val filledCurrencies = listOf( mockk { every { network } returns mockk() }, @@ -102,7 +102,7 @@ class AccountTest { name = name, icon = mockk(), derivationIndex = 0, - cryptoCurrencies = emptySet(), + cryptoCurrencies = emptyList(), ) .leftOrNull()!! @@ -123,7 +123,7 @@ class AccountTest { name = "Test Account", icon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")), derivationIndex = derivationIndex.value, - cryptoCurrencies = emptySet(), + cryptoCurrencies = emptyList(), ) .getOrNull()!! @@ -150,7 +150,7 @@ class AccountTest { accountName = AccountName.DefaultMain, icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), derivationIndex = derivationIndex, - cryptoCurrencies = emptySet(), + cryptoCurrencies = emptyList(), ) Truth.assertThat(actual).isEqualTo(expected) @@ -161,7 +161,7 @@ class AccountTest { userWalletId: UserWalletId = UserWalletId("011"), name: String = "Test Account", derivationIndex: Int = 0, - currencies: Set = emptySet(), + currencies: List = emptyList(), ): CryptoPortfolio { val accountIndex = DerivationIndex(value = derivationIndex).getOrNull()!! diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt index 0d855a3880..2bc39f10d0 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt @@ -212,7 +212,7 @@ internal class TesterAccountsViewModel @Inject constructor( name = "Account #$nextIndex", icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), derivationIndex = nextIndex, - cryptoCurrencies = emptySet(), + cryptoCurrencies = emptyList(), ) .getOrNull() ?: break diff --git a/test/mock/src/main/java/com/tangem/test/mock/MockAccounts.kt b/test/mock/src/main/java/com/tangem/test/mock/MockAccounts.kt index f275e2b0d2..d2bdca9cb0 100644 --- a/test/mock/src/main/java/com/tangem/test/mock/MockAccounts.kt +++ b/test/mock/src/main/java/com/tangem/test/mock/MockAccounts.kt @@ -46,7 +46,7 @@ object MockAccounts { derivationIndex: Int, name: String = "Account #$derivationIndex", icon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - cryptoCurrencies: Set = emptySet(), + cryptoCurrencies: List = emptyList(), userWalletId: UserWalletId = this.userWalletId, ): Account.CryptoPortfolio { val derivationIndex = DerivationIndex(derivationIndex).getOrNull()!! From bbce67bcd2b4ed3dc6f44a1cbd5882fbaf43f832 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Feb 2026 12:43:15 +0300 Subject: [PATCH 047/106] Updated on 2026-08-14 --- .../ui/markets/models/MarketsListItemUM.kt | 8 +++++++ .../api/markets/TangemTechMarketsApi.kt | 1 + .../response/TokenMarketListResponse.kt | 8 +++++++ .../markets/DefaultMarketsTokenRepository.kt | 1 + .../converters/TokenMarketListConverter.kt | 7 ++++++ .../com/tangem/domain/markets/TokenMarket.kt | 7 ++++++ .../domain/markets/TokenMarketListConfig.kt | 1 + .../market/SwapMarketsListBatchFlowManager.kt | 1 + .../SwapMarketsTokenItemConverter.kt | 7 ++++++ .../tangem/feature/swap/model/SwapModel.kt | 23 ++++++++++--------- .../market/SwapMarketsListBatchFlowManager.kt | 8 +++++++ .../SwapMarketsTokenItemConverter.kt | 7 ++++++ 12 files changed, 68 insertions(+), 11 deletions(-) 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 9d48452aef..7659617208 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 @@ -22,6 +22,7 @@ data class MarketsListItemUM( val isUnder100kMarketCap: Boolean, val stakingRate: TextReference?, val updateTimestamp: Long?, + val networks: List? = null, ) { val chartType: MarketChartLook.Type = when (trendType) { PriceChangeType.UP -> MarketChartLook.Type.Growing @@ -29,6 +30,13 @@ data class MarketsListItemUM( PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral } + @Immutable + data class Network( + val networkId: String, + val contractAddress: String?, + val decimalCount: Int?, + ) + @Immutable data class Price( val text: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt index eaeb531130..7b666f4171 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt @@ -18,6 +18,7 @@ interface TangemTechMarketsApi { @Query("order") order: String, @Query("search") search: String?, @Query("timestamp") timestamp: Long?, + @Query("showNetworks") showNetworks: Boolean? = null, ): ApiResponse @GET("v1/coins/{coin_id}") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt index 31168b8c5b..bd241731d6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt @@ -27,8 +27,16 @@ data class TokenMarketListResponse( @Json(name = "is_under_market_cap_limit") val isUnderMarketCapLimit: Boolean?, @Json(name = "staking_opportunities") val stakingOpportunities: List?, @Json(name = "max_yield_apy") val maxYieldApy: BigDecimal?, + @Json(name = "networks") val networks: List? = null, ) { + @JsonClass(generateAdapter = true) + data class Network( + @Json(name = "network_id") val networkId: String, + @Json(name = "contract_address") val contractAddress: String?, + @Json(name = "decimal_count") val decimalCount: Int?, + ) + @JsonClass(generateAdapter = true) data class PriceChangePercentage( @Json(name = "24h") val h24: BigDecimal?, diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 834c80a691..12d5b0f694 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -75,6 +75,7 @@ internal class DefaultMarketsTokenRepository( offset = request.offset, limit = request.limit, timestamp = if (isFirstBatchFetching) null else requestTimeStamp.get(), + showNetworks = request.params.shouldNetworks, ).getOrThrow() } diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt index f43e7ddae1..86b29f8006 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -42,6 +42,13 @@ internal object TokenMarketListConverter : Converter + TokenMarket.Network( + networkId = network.networkId, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) + }, ) } return TokenMarketListWithMaxApy(tokens, value.summary?.maxApy) diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt index 3deeee528d..5d6463b790 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt @@ -14,9 +14,16 @@ data class TokenMarket( val tokenCharts: Charts, val yieldRate: BigDecimal?, val updateTimestamp: Long?, + val networks: List?, private val imageHost: String, ) { + data class Network( + val networkId: String, + val contractAddress: String?, + val decimalCount: Int?, + ) + data class Charts( val h24: TokenChart?, val week: TokenChart?, diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt index ffd82b8909..c1a1d9631f 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt @@ -5,6 +5,7 @@ data class TokenMarketListConfig( val searchText: String?, val priceChangeInterval: Interval, val order: Order, + val shouldNetworks: Boolean? = null, ) { enum class Order { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt index e25d1bcc4a..2aecafa4e4 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt @@ -184,6 +184,7 @@ internal class SwapMarketsListBatchFlowManager( }, priceChangeInterval = TokenMarketListConfig.Interval.H24, order = TokenMarketListConfig.Order.ByRating, + shouldNetworks = true, ), ), ) 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 6ea8ec9f3c..f76ce2af6d 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 @@ -44,6 +44,13 @@ internal class SwapMarketsTokenItemConverter( resourceReference(R.string.markets_apy_placeholder, wrappedList(it)) }, updateTimestamp = value.updateTimestamp, + networks = value.networks?.map { network -> + MarketsListItemUM.Network( + networkId = network.networkId, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) + }, ) } 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 1448ec5de8..35c5fd4fa3 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 @@ -43,7 +43,8 @@ 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.GetTokenMarketInfoUseCase +import com.tangem.domain.markets.TokenMarketInfo +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 @@ -73,7 +74,6 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.swap.analytics.StoriesEvents import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent -import com.tangem.feature.swap.converters.TokenMarketInfoToParamsConverter import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.TransactionFeeResult import com.tangem.feature.swap.domain.TxFeeSealedState @@ -156,7 +156,6 @@ internal class SwapModel @Inject constructor( private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, swapFeatureToggles: SwapFeatureToggles, private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, - private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase, private val excludedBlockchains: ExcludedBlockchains, private val getUserWalletsUseCase: GetWalletsUseCase, private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase, @@ -2061,23 +2060,25 @@ internal class SwapModel @Inject constructor( private fun addToPortfolioItem(item: MarketsListItemUM) { modelScope.launch { - val tokenInfo = getTokenMarketInfoUseCase( - selectedAppCurrencyFlow.value, - item.id, - item.currencySymbol, - ).getOrNull() ?: return@launch + val tokenMarket = searchMarketsListManager.getTokenMarketById(item.id) ?: return@launch - val converter = TokenMarketInfoToParamsConverter() - val param = converter.convert(tokenInfo) + val param = tokenMarket.toSerializableParam() val hasOnlyHotWallets = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot } - val networks = tokenInfo.networks?.filter { network -> + val networks = tokenMarket.networks?.filter { network -> BlockchainUtils.isSupportedNetworkId( blockchainId = network.networkId, excludedBlockchains = excludedBlockchains, hotExcludedBlockchains = hotWalletExcludedBlockchains, hasOnlyHotWallets = hasOnlyHotWallets, ) + }?.map { network -> + TokenMarketInfo.Network( + networkId = network.networkId, + isExchangeable = false, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) }.orEmpty() addToPortfolioManager = addToPortfolioManagerFactory 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/SwapMarketsListBatchFlowManager.kt index 3e7b9af39f..e8331efed3 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/SwapMarketsListBatchFlowManager.kt @@ -184,6 +184,7 @@ internal class SwapMarketsListBatchFlowManager( }, priceChangeInterval = TokenMarketListConfig.Interval.H24, order = TokenMarketListConfig.Order.ByRating, + shouldNetworks = true, ), ), ) @@ -236,6 +237,13 @@ internal class SwapMarketsListBatchFlowManager( .toSet() } + fun getTokenMarketById(id: CryptoCurrency.RawID): TokenMarket? { + return batchFlow.state.value.data + .asSequence() + .flatMap { it.data } + .firstOrNull { it.id == id } + } + private data class ResultBatches( val uiBatches: List>> = emptyList(), val processedItems: List>>? = null, 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 78c52b2aab..31ea67416e 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 @@ -44,6 +44,13 @@ internal class SwapMarketsTokenItemConverter( resourceReference(R.string.markets_apy_placeholder, wrappedList(it)) }, updateTimestamp = value.updateTimestamp, + networks = value.networks?.map { network -> + MarketsListItemUM.Network( + networkId = network.networkId, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) + }, ) } From d66e528975e280e74a4f1d3ba7446cb466f3d5a6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 13:34:10 +0500 Subject: [PATCH 048/106] Updated on 2026-08-14 --- .../assets/configs/feature_toggles_config.json | 4 ---- .../tangem/features/nft/NFTFeatureToggles.kt | 6 ------ .../features/nft/DefaultNFTFeatureToggles.kt | 11 ----------- .../tangem/features/nft/di/NFTFeatureModule.kt | 17 +---------------- 4 files changed, 1 insertion(+), 37 deletions(-) delete mode 100644 features/nft/api/src/main/kotlin/com/tangem/features/nft/NFTFeatureToggles.kt delete mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/DefaultNFTFeatureToggles.kt 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 399317f1ee..cdbd47714a 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -11,10 +11,6 @@ "name": "STAKING_TON_ENABLED", "version": "5.28.0" }, - { - "name": "NFT_MEDIA_CONTENT_ENABLED", - "version": "undefined" - }, { "name": "STAKING_CARDANO_ENABLED", "version": "5.31.1" diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/NFTFeatureToggles.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/NFTFeatureToggles.kt deleted file mode 100644 index 36689ae915..0000000000 --- a/features/nft/api/src/main/kotlin/com/tangem/features/nft/NFTFeatureToggles.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.features.nft - -interface NFTFeatureToggles { - - val isNFTMediaContentEnabled: Boolean -} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/DefaultNFTFeatureToggles.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/DefaultNFTFeatureToggles.kt deleted file mode 100644 index 6ee99738ee..0000000000 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/DefaultNFTFeatureToggles.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.features.nft - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager - -internal class DefaultNFTFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : NFTFeatureToggles { - - override val isNFTMediaContentEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "NFT_MEDIA_CONTENT_ENABLED") -} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt index 1b3221badd..9d85522a2c 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt @@ -1,9 +1,6 @@ package com.tangem.features.nft.di -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.decompose.model.Model -import com.tangem.features.nft.DefaultNFTFeatureToggles -import com.tangem.features.nft.NFTFeatureToggles import com.tangem.features.nft.collections.model.NFTCollectionsModel import com.tangem.features.nft.common.DefaultNFTComponent import com.tangem.features.nft.component.* @@ -18,7 +15,6 @@ import com.tangem.features.nft.receive.model.NFTReceiveModel import com.tangem.features.nft.traits.model.NFTAssetTraitsModel import dagger.Binds import dagger.Module -import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.ClassKey @@ -27,18 +23,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object NFTFeatureModule { - - @Provides - @Singleton - fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): NFTFeatureToggles { - return DefaultNFTFeatureToggles(featureTogglesManager) - } -} - -@Module -@InstallIn(SingletonComponent::class) -internal interface NFTFeatureModuleBinds { +internal interface NFTFeatureModule { @Binds @Singleton fun bindNFTDetailsInfoComponentFactory( From ac7d3f1607ad920425c5b3b4fa0ee2677c147928 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 09:37:22 +0100 Subject: [PATCH 049/106] Updated on 2026-08-14 --- .../AddToPortfolioPreselectedDataComponent.kt | 3 + .../components/earn/DefaultEarnComponent.kt | 1 + .../components/feed/DefaultFeedComponent.kt | 4 + .../components/feed/FeedBottomSheetRoute.kt | 5 +- .../AddToPortfolioPreselectedDataModel.kt | 22 ++++ .../features/feed/model/earn/EarnModel.kt | 34 +++++- .../earn/analytics/EarnAnalyticsEvent.kt | 102 ++++++++++++++++++ .../earn/filters/EarnNetworkFilterModel.kt | 24 ++++- .../model/earn/filters/EarnTypeFilterModel.kt | 22 +++- .../model/earn/state/EarnStateController.kt | 5 +- .../UpdateEarnUMInitialStateTransformer.kt | 2 + .../UpdateMostlyUsedStateTransformer.kt | 7 +- .../statemanager/EarnListBatchFlowManager.kt | 7 +- .../feed/model/feed/FeedComponentModel.kt | 20 +++- .../feed/analytics/FeedAnalyticsEvent.kt | 16 +++ .../UpdateEarnStateTransformer.kt | 24 ++++- .../features/feed/ui/earn/EarnContent.kt | 28 +++-- .../features/feed/ui/earn/state/EarnUM.kt | 1 + 18 files changed, 301 insertions(+), 26 deletions(-) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt index f901b35f54..c7feac3a8c 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt @@ -16,8 +16,11 @@ interface AddToPortfolioPreselectedDataComponent : ComposableBottomSheetComponen data class Params( val tokenToAdd: TokenToAdd, val callback: Callback, + val analyticsParams: AnalyticsParams, ) + data class AnalyticsParams(val source: String) + interface Callback { fun onDismiss() fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId) 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 679bc440fa..a0b7dcb1e8 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 @@ -77,6 +77,7 @@ internal class DefaultEarnComponent( params = AddToPortfolioPreselectedDataComponent.Params( tokenToAdd = config.tokenToAdd, callback = earnModel.addToPortfolioCallback, + analyticsParams = AddToPortfolioPreselectedDataComponent.AnalyticsParams(config.source), ), ) } 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 fba9eedfbe..7fbaa5c4fe 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 @@ -9,6 +9,7 @@ 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.analytics.models.AnalyticsParam import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel @@ -75,6 +76,9 @@ internal class DefaultFeedComponent( params = Params( tokenToAdd = config.tokenToAdd, callback = feedComponentModel.addToPortfolioCallback, + analyticsParams = AddToPortfolioPreselectedDataComponent.AnalyticsParams( + AnalyticsParam.ScreensSources.Markets.value, + ), ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt index 6a2ad72b81..fea9189d03 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt @@ -6,7 +6,10 @@ import com.tangem.features.feed.components.market.details.portfolio.add.AddToPor internal sealed interface FeedBottomSheetRoute { - data class AddToPortfolio(val tokenToAdd: AddToPortfolioPreselectedDataComponent.TokenToAdd) : FeedBottomSheetRoute + data class AddToPortfolio( + val tokenToAdd: AddToPortfolioPreselectedDataComponent.TokenToAdd, + val source: String, + ) : FeedBottomSheetRoute data class NetworkFilter(val params: EarnNetworkFilterComponent.Params) : FeedBottomSheetRoute 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 8beb2880d3..79ca66cf2b 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 @@ -25,6 +25,7 @@ import com.tangem.features.feed.components.market.details.portfolio.add.* import com.tangem.features.feed.components.market.details.portfolio.add.impl.AddTokenComponent import com.tangem.features.feed.components.market.details.portfolio.impl.analytics.PortfolioAnalyticsEvent import com.tangem.features.feed.impl.R +import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* @@ -125,9 +126,11 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor( return@channelFlow } + sendAddTokenOpenedAnalytics(selectedNetworkValue.cryptoCurrency) navigation.replaceAll(AddToPortfolioRoutes.AddToken) val addedToken = callbackDelegate.onTokenAdded.receiveAsFlow().first() messageSender.send(ToastMessage(message = resourceReference(R.string.markets_token_added))) + sendSuccessAddedAnalytics(addedToken.currency) finishSuccessFlow(addedToken.currency, selectedPortfolioValue.userWallet.walletId) } .catch { throwable -> @@ -267,6 +270,25 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor( }, ) .filterNotNull() + + private fun sendSuccessAddedAnalytics(cryptoCurrency: CryptoCurrency) { + analyticsEventHandler.send( + EarnAnalyticsEvent.TokenAdded( + tokenSymbol = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + ), + ) + } + + private fun sendAddTokenOpenedAnalytics(cryptoCurrency: CryptoCurrency) { + analyticsEventHandler.send( + EarnAnalyticsEvent.AddTokenScreenOpened( + tokenSymbol = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + source = params.analyticsParams.source, + ), + ) + } } @ModelScoped 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 a26dd26493..bb91b6a704 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 @@ -7,9 +7,11 @@ import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.domain.earn.model.EarnFilter import com.tangem.domain.earn.model.EarnFilterNetwork import com.tangem.domain.earn.model.EarnFilterType @@ -24,6 +26,7 @@ import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent import com.tangem.features.feed.components.earn.EarnTypeFilterComponent 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.analytics.EarnAnalyticsEvent import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkConverter import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConverter import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeConverter @@ -59,6 +62,7 @@ internal class EarnModel @Inject constructor( private val setEarnFilterUseCase: SetEarnFilterUseCase, private val appRouter: AppRouter, private val stateController: EarnStateController, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() @@ -115,6 +119,7 @@ internal class EarnModel @Inject constructor( ) { items, error, paginationStatus -> val hasActiveFilters = state.value.selectedTypeFilter != EarnFilterTypeUM.All || state.value.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks + error?.let(::handleBestOpportunitiesErrorAnalytics) EarnListStateManager.calculateState( items = items, error = error, @@ -242,7 +247,14 @@ internal class EarnModel @Inject constructor( } } - private fun onEarnTokenClick(earnTokenWithCurrency: EarnTokenWithCurrency) { + private fun onEarnTokenClick(earnTokenWithCurrency: EarnTokenWithCurrency, source: String) { + analyticsEventHandler.send( + EarnAnalyticsEvent.OpportunitySelected( + tokenSymbol = earnTokenWithCurrency.earnToken.tokenSymbol, + blockchain = earnTokenWithCurrency.cryptoCurrency.network.name, + source = source, + ), + ) bottomSheetNavigation.activate( FeedBottomSheetRoute.AddToPortfolio( tokenToAdd = AddToPortfolioPreselectedDataComponent.TokenToAdd( @@ -256,6 +268,7 @@ internal class EarnModel @Inject constructor( name = earnTokenWithCurrency.earnToken.tokenName, symbol = earnTokenWithCurrency.earnToken.tokenSymbol, ), + source = source, ), ) } @@ -283,14 +296,33 @@ internal class EarnModel @Inject constructor( } bottomSheetNavigation.dismiss() } + + private fun onMostlyUsedScrolled() { + analyticsEventHandler.send(EarnAnalyticsEvent.MostlyUsedCarouselScrolled()) + } /* end of clicks area */ private fun updateInitialState() { + analyticsEventHandler.send(EarnAnalyticsEvent.EarnOpened()) stateController.update( UpdateEarnUMInitialStateTransformer( onBackClick = params.onBackClick, onNetworkFilterClick = ::onNetworkFilterClick, onTypeFilterClick = ::onTypeFilterClick, + onScroll = ::onMostlyUsedScrolled, + ), + ) + } + + private fun handleBestOpportunitiesErrorAnalytics(error: Throwable) { + val (code, message) = when (error) { + is ApiResponseError.HttpException -> error.code.numericCode to error.message.orEmpty() + else -> null to "" + } + analyticsEventHandler.send( + EarnAnalyticsEvent.BestOpportunitiesLoadError( + code = code, + message = message, ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt new file mode 100644 index 0000000000..c608bc31c7 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt @@ -0,0 +1,102 @@ +package com.tangem.features.feed.model.earn.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE +import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE +import com.tangem.core.analytics.models.IS_NOT_HTTP_ERROR +import com.tangem.core.analytics.models.OneTimePerSessionEvent + +internal sealed class EarnAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category = "Earn", event = event, params = params) { + + class EarnOpened : EarnAnalyticsEvent(event = "Page Opened") + + class MostlyUsedCarouselScrolled : EarnAnalyticsEvent(event = "Mostly Used Carousel Scrolled"), + OneTimePerSessionEvent { + override val oneTimeEventId: String = event + } + + data class BestOpportunitiesFilterNetworkApplied( + private val networkId: String?, + private val filterType: FilterNetworkAnalytic, + ) : EarnAnalyticsEvent( + event = "Best Opportunities Filter Network Applied", + params = mapOf( + "Network Filter Type" to filterType.value, + "NetworkId" to networkId.orEmpty(), + ), + ) + + data class BestOpportunitiesFilterTypeApplied( + private val filterTypeAnalytic: FilterTypeAnalytic, + ) : EarnAnalyticsEvent( + event = "Best Opportunities Filter Type Applied", + params = mapOf("Type" to filterTypeAnalytic.value), + ) + + data class OpportunitySelected( + private val tokenSymbol: String, + private val blockchain: String, + private val source: String, + ) : EarnAnalyticsEvent( + event = "Opportunity selected", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to tokenSymbol, + AnalyticsParam.BLOCKCHAIN to blockchain, + AnalyticsParam.SOURCE to source, + ), + ) + + data class AddTokenScreenOpened( + private val tokenSymbol: String, + private val blockchain: String, + private val source: String, + ) : EarnAnalyticsEvent( + event = "Add Token Screen Opened", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to tokenSymbol, + AnalyticsParam.BLOCKCHAIN to blockchain, + AnalyticsParam.SOURCE to source, + ), + ) + + data class TokenAdded( + private val tokenSymbol: String, + private val blockchain: String, + ) : EarnAnalyticsEvent( + event = "Token Added", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to tokenSymbol, + AnalyticsParam.BLOCKCHAIN to blockchain, + ), + ) + + data class BestOpportunitiesLoadError( + private val code: Int?, + private val message: String, + ) : EarnAnalyticsEvent( + event = "Best Opportunities Load Error", + params = mapOf( + ERROR_CODE to (code ?: IS_NOT_HTTP_ERROR).toString(), + ERROR_MESSAGE to message, + ), + ) +} + +internal const val BEST_OPPORTUNITIES_SOURCE = "Best Opportunity" +internal const val MOSTLY_USED_SOURCE = "Mostly Used" + +internal enum class FilterNetworkAnalytic(val value: String) { + ALL_NETWORKS("All Networks"), + MY_NETWORKS("My Networks"), + SPECIFIC("Specific"), +} + +internal enum class FilterTypeAnalytic(val value: String) { + YIELD("Yield"), + STAKING("Staking"), + ALL_TYPES("All Types"), +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnNetworkFilterModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnNetworkFilterModel.kt index 47f92e70d1..793c286f54 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnNetworkFilterModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnNetworkFilterModel.kt @@ -1,13 +1,17 @@ package com.tangem.features.feed.model.earn.filters import androidx.compose.runtime.Stable +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent -import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConverter +import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent +import com.tangem.features.feed.model.earn.analytics.FilterNetworkAnalytic import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkConverter +import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConverter import com.tangem.features.feed.ui.earn.state.EarnFilterByNetworkBottomSheetContentUM +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableStateFlow @@ -18,6 +22,7 @@ import javax.inject.Inject @ModelScoped internal class EarnNetworkFilterModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, ) : Model() { @@ -31,7 +36,22 @@ internal class EarnNetworkFilterModel @Inject constructor( networks = params.allFilters .map { EarnFilterNetworkConverter().convert(it) } .toPersistentList(), - onOptionClick = { filterUM -> params.onFilterSelected(EarnFilterNetworkUMConverter().convert(filterUM)) }, + onOptionClick = ::handleOnOptionClick, + ) + } + + private fun handleOnOptionClick(filter: EarnFilterNetworkUM) { + params.onFilterSelected(EarnFilterNetworkUMConverter().convert(filter)) + val (networkId, filterType) = when (filter) { + is EarnFilterNetworkUM.AllNetworks -> "" to FilterNetworkAnalytic.ALL_NETWORKS + is EarnFilterNetworkUM.MyNetworks -> "" to FilterNetworkAnalytic.MY_NETWORKS + is EarnFilterNetworkUM.Network -> filter.id to FilterNetworkAnalytic.SPECIFIC + } + analyticsEventHandler.send( + EarnAnalyticsEvent.BestOpportunitiesFilterNetworkApplied( + networkId = networkId, + filterType = filterType, + ), ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnTypeFilterModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnTypeFilterModel.kt index 01e2454dad..11414b134f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnTypeFilterModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnTypeFilterModel.kt @@ -1,13 +1,17 @@ package com.tangem.features.feed.model.earn.filters import androidx.compose.runtime.Stable +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.features.feed.components.earn.EarnTypeFilterComponent +import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent +import com.tangem.features.feed.model.earn.analytics.FilterTypeAnalytic import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeConverter import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeUMConverter import com.tangem.features.feed.ui.earn.state.EarnFilterByTypeBottomSheetContentUM +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -17,6 +21,7 @@ import javax.inject.Inject @ModelScoped internal class EarnTypeFilterModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, paramsContainer: ParamsContainer, ) : Model() { @@ -28,9 +33,20 @@ internal class EarnTypeFilterModel @Inject constructor( private fun configureInitialState(): EarnFilterByTypeBottomSheetContentUM { return EarnFilterByTypeBottomSheetContentUM( selectedOption = EarnFilterTypeConverter().convert(params.selectedFilter), - onOptionClick = { filterTypeUM -> - params.onFilterSelected(EarnFilterTypeUMConverter().convert(filterTypeUM)) - }, + onOptionClick = ::handleOnOptionClick, + ) + } + + private fun handleOnOptionClick(filter: EarnFilterTypeUM) { + params.onFilterSelected(EarnFilterTypeUMConverter().convert(filter)) + analyticsEventHandler.send( + EarnAnalyticsEvent.BestOpportunitiesFilterTypeApplied( + filterTypeAnalytic = when (filter) { + EarnFilterTypeUM.All -> FilterTypeAnalytic.ALL_TYPES + EarnFilterTypeUM.Staking -> FilterTypeAnalytic.STAKING + EarnFilterTypeUM.YieldMode -> FilterTypeAnalytic.YIELD + }, + ), ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt index 4af99089f3..6cec5cd393 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 @@ -18,10 +18,6 @@ internal class EarnStateController @Inject constructor() { val value: EarnUM get() = uiState.value - fun update(function: (EarnUM) -> EarnUM) { - mutableUiState.update(function = function) - } - fun update(transformer: EarnUMTransformer) { mutableUiState.update(function = transformer::transform) } @@ -35,6 +31,7 @@ internal class EarnStateController @Inject constructor() { onBackClick = {}, onNetworkFilterClick = {}, onTypeFilterClick = {}, + onSliderScroll = {}, ) } } \ 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 f60b57a8e0..d46ac02956 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 @@ -6,6 +6,7 @@ internal class UpdateEarnUMInitialStateTransformer( private val onBackClick: () -> Unit, private val onNetworkFilterClick: () -> Unit, private val onTypeFilterClick: () -> Unit, + private val onScroll: () -> Unit, ) : EarnUMTransformer { override fun transform(prevState: EarnUM): EarnUM { @@ -13,6 +14,7 @@ internal class UpdateEarnUMInitialStateTransformer( onBackClick = onBackClick, onNetworkFilterClick = onNetworkFilterClick, onTypeFilterClick = onTypeFilterClick, + onSliderScroll = onScroll, ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt index 9f280044d9..a3a8317268 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.features.feed.model.earn.state.transformers import com.tangem.domain.models.earn.EarnTokenWithCurrency import com.tangem.domain.models.earn.EarnTopToken import com.tangem.features.feed.model.converter.EarnTokenWithCurrencyToListItemUMConverter +import com.tangem.features.feed.model.earn.analytics.MOSTLY_USED_SOURCE import com.tangem.features.feed.ui.earn.state.EarnListUM import com.tangem.features.feed.ui.earn.state.EarnUM import kotlinx.collections.immutable.toPersistentList @@ -10,11 +11,13 @@ import java.math.BigDecimal internal class UpdateMostlyUsedStateTransformer( private val earnResult: EarnTopToken?, - private val onItemClick: (EarnTokenWithCurrency) -> Unit, + private val onItemClick: (EarnTokenWithCurrency, source: String) -> Unit, private val onRetryClick: () -> Unit, ) : EarnUMTransformer { - private val converter = EarnTokenWithCurrencyToListItemUMConverter(onItemClick = onItemClick) + private val converter = EarnTokenWithCurrencyToListItemUMConverter( + onItemClick = { token -> onItemClick(token, MOSTLY_USED_SOURCE) }, + ) override fun transform(prevState: EarnUM): EarnUM { return when (earnResult) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt index 685771c34d..c241d58af4 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt @@ -5,6 +5,7 @@ import com.tangem.domain.earn.model.EarnTokensListConfig import com.tangem.domain.earn.usecase.GetEarnTokensBatchFlowUseCase import com.tangem.domain.models.earn.EarnTokenWithCurrency import com.tangem.features.feed.model.converter.EarnTokenWithCurrencyToListItemUMConverter +import com.tangem.features.feed.model.earn.analytics.BEST_OPPORTUNITIES_SOURCE import com.tangem.features.feed.ui.earn.state.EarnListItemUM import com.tangem.pagination.BatchAction import com.tangem.pagination.PaginationStatus @@ -20,12 +21,14 @@ import kotlinx.coroutines.launch internal class EarnListBatchFlowManager( getEarnTokensBatchFlowUseCase: GetEarnTokensBatchFlowUseCase, private val configProvider: Provider, - private val onItemClick: (EarnTokenWithCurrency) -> Unit, + private val onItemClick: (EarnTokenWithCurrency, source: String) -> Unit, private val modelScope: CoroutineScope, private val dispatchers: CoroutineDispatcherProvider, ) { private val actionsFlow = MutableSharedFlow>() - private val converter = EarnTokenWithCurrencyToListItemUMConverter(onItemClick = onItemClick) + private val converter = EarnTokenWithCurrencyToListItemUMConverter( + onItemClick = { onItemClick(it, BEST_OPPORTUNITIES_SOURCE) }, + ) private val batchFlow = getEarnTokensBatchFlowUseCase( context = EarnTokensBatchingContext( 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 be776002e3..fde61b8825 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 @@ -33,6 +33,7 @@ import com.tangem.features.feed.components.feed.FeedBottomSheetRoute import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.feed.impl.R +import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent import com.tangem.features.feed.model.feed.state.FeedMarketsBatchFlowManager import com.tangem.features.feed.model.feed.state.FeedStateController @@ -172,6 +173,7 @@ internal class FeedComponentModel @Inject constructor( onItemClick = ::handleEarnTokenClick, onRetryClick = ::fetchEarnData, earnResult = earnResult, + analyticsEventHandler = analyticsEventHandler, ), ) } @@ -249,7 +251,7 @@ internal class FeedComponentModel @Inject constructor( onSliderEndReached = { analyticsEventHandler.send(FeedAnalyticsEvent.NewsCarouselEndReached()) }, - onOpenEarnPageClick = params.feedClickIntents::onOpenEarnPage, + onOpenEarnPageClick = ::handleEarnPageOpenClicked, ), news = NewsUM( content = persistentListOf(), @@ -294,7 +296,6 @@ internal class FeedComponentModel @Inject constructor( onMarketOpenClick = ::handleMarketOpenClicked, onArticleClick = ::handleArticleClicked, onOpenAllNews = ::handleOpenAllNews, - onOpenEarnPageClick = params.feedClickIntents::onOpenEarnPage, ), ) } @@ -397,6 +398,13 @@ internal class FeedComponentModel @Inject constructor( } private fun handleEarnTokenClick(earnTokenWithCurrency: EarnTokenWithCurrency) { + analyticsEventHandler.send( + EarnAnalyticsEvent.OpportunitySelected( + tokenSymbol = earnTokenWithCurrency.earnToken.tokenSymbol, + blockchain = earnTokenWithCurrency.cryptoCurrency.network.name, + source = AnalyticsParam.ScreensSources.Markets.value, + ), + ) bottomSheetNavigation.activate( FeedBottomSheetRoute.AddToPortfolio( tokenToAdd = AddToPortfolioPreselectedDataComponent.TokenToAdd( @@ -410,9 +418,17 @@ internal class FeedComponentModel @Inject constructor( name = earnTokenWithCurrency.earnToken.tokenName, symbol = earnTokenWithCurrency.earnToken.tokenSymbol, ), + source = AnalyticsParam.ScreensSources.Markets.value, ), ) } + + private fun handleEarnPageOpenClicked() { + if (state.value.earnListUM is EarnListUM.Content) { + params.feedClickIntents.onOpenEarnPage() + analyticsEventHandler.send(FeedAnalyticsEvent.EarnScreenOpened()) + } + } /* end of clicks area */ /* start of utils area */ diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/analytics/FeedAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/analytics/FeedAnalyticsEvent.kt index e40a8503c2..b6279ab0cb 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/analytics/FeedAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/analytics/FeedAnalyticsEvent.kt @@ -83,4 +83,20 @@ internal sealed class FeedAnalyticsEvent( class AllWidgetsLoadError : FeedAnalyticsEvent(event = "All Widgets Load Error") class TokenSearchedClicked : FeedAnalyticsEvent(event = "Token Searched Clicked") + + class EarnScreenOpened : FeedAnalyticsEvent( + event = "Earn Screen Opened", + params = mapOf(SOURCE to AnalyticsParam.ScreensSources.Markets.value), + ) + + data class EarnLoadError( + private val code: Int?, + private val message: String, + ) : FeedAnalyticsEvent( + event = "Earn Load Error", + params = mapOf( + ERROR_CODE to (code ?: IS_NOT_HTTP_ERROR).toString(), + ERROR_MESSAGE to message, + ), + ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt index 3d4594e1eb..fd81cc6428 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt @@ -1,8 +1,11 @@ package com.tangem.features.feed.model.feed.state.transformers +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.models.earn.EarnError import com.tangem.domain.models.earn.EarnTokenWithCurrency import com.tangem.domain.models.earn.EarnTopToken import com.tangem.features.feed.model.converter.EarnTokenWithCurrencyToListItemUMConverter +import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent import com.tangem.features.feed.ui.earn.state.EarnListUM import com.tangem.features.feed.ui.feed.state.FeedListUM import kotlinx.collections.immutable.toPersistentList @@ -13,6 +16,7 @@ internal class UpdateEarnStateTransformer( private val onItemClick: (EarnTokenWithCurrency) -> Unit, private val onRetryClick: () -> Unit, private val earnResult: EarnTopToken?, + private val analyticsEventHandler: AnalyticsEventHandler, ) : FeedListUMTransformer { private val earnTokenWithCurrencyConverter = EarnTokenWithCurrencyToListItemUMConverter(onItemClick = onItemClick) @@ -25,8 +29,11 @@ internal class UpdateEarnStateTransformer( } else -> { earnResult.fold( - ifLeft = { - handleErrorState(prevState) + ifLeft = { earnError -> + handleErrorState( + currentState = prevState, + result = earnError, + ) }, ifRight = { earnTokensWithCurrency -> handleDataState( @@ -43,7 +50,18 @@ internal class UpdateEarnStateTransformer( return currentState.copy(earnListUM = null) } - private fun handleErrorState(currentState: FeedListUM): FeedListUM { + private fun handleErrorState(currentState: FeedListUM, result: EarnError): FeedListUM { + if (currentState.earnListUM != EarnListUM.Loading) return currentState + val (code, message) = when (result) { + is EarnError.HttpError -> result.code to result.message + is EarnError.NotHttpError -> null to "" + } + analyticsEventHandler.send( + FeedAnalyticsEvent.EarnLoadError( + code = code, + message = message, + ), + ) return currentState.copy(earnListUM = EarnListUM.Error(onRetryClick)) } 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 86139c45f7..0b7845919d 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 @@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onFirstVisible import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign @@ -73,7 +74,10 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { } item(key = "mostly_used_content") { - MostlyUsedContent(state = state.mostlyUsed) + MostlyUsedContent( + state = state.mostlyUsed, + onScroll = state.onSliderScroll, + ) } item(key = "best_opportunities_header") { @@ -105,7 +109,7 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { } @Composable -private fun MostlyUsedContent(state: EarnListUM) { +private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) { AnimatedContent( targetState = state, contentKey = { it::class.java }, @@ -122,11 +126,21 @@ private fun MostlyUsedContent(state: EarnListUM) { ), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - items( + itemsIndexed( items = st.items, - key = { "${it.tokenName}-${it.network}" }, - ) { item -> + key = { _, item -> "${item.tokenName}-${item.network}" }, + ) { index, item -> + val cardModifier = Modifier.conditional( + condition = index == FOURTH_ITEM_INDEX, + modifier = { + onFirstVisible( + minFractionVisible = 0.5f, + callback = onScroll, + ) + }, + ) MostlyUsedCard( + modifier = cardModifier, item = item, onClick = item.onItemClick, ) @@ -579,6 +593,8 @@ private fun previewEarnUM( onBackClick = {}, onNetworkFilterClick = {}, onTypeFilterClick = {}, + onSliderScroll = {}, ) -private const val PLACEHOLDER_ITEMS_COUNT = 8 \ No newline at end of file +private const val PLACEHOLDER_ITEMS_COUNT = 8 +private const val FOURTH_ITEM_INDEX = 3 \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt index b0686ee0e8..a11f66f48e 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 @@ -12,6 +12,7 @@ internal data class EarnUM( val onBackClick: () -> Unit, val onNetworkFilterClick: () -> Unit, val onTypeFilterClick: () -> Unit, + val onSliderScroll: () -> Unit, ) { val selectedTypeFilterText: TextReference From 44abe1e0ccd6e66827b5fccabba99562f925ac9c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 11:43:52 +0300 Subject: [PATCH 050/106] Updated on 2026-08-14 --- .../core/ui/components/token/TokenItem.kt | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index d76f2160fb..8b411abf19 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -29,10 +29,7 @@ import com.tangem.core.ui.components.icons.IconTint import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.token.internal.* import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState -import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State -import com.tangem.core.ui.components.token.state.TokenItemState.PromoBannerState -import com.tangem.core.ui.components.token.state.TokenItemState.TitleState +import com.tangem.core.ui.components.token.state.TokenItemState.* import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.extensions.stringReference @@ -519,15 +516,14 @@ private fun calculateLayoutHeight( is TokenItemState.Content, is TokenItemState.Loading, is TokenItemState.Locked, - -> { - firstColumnHeight = 2 * layoutPadding + title.height + (cryptoAmount?.height ?: 0) - secondColumnHeight = 2 * layoutPadding + (fiatAmount?.height ?: 0) + (priceChange?.height ?: 0) - } - is TokenItemState.Draggable, is TokenItemState.NoAddress, is TokenItemState.Unreachable, -> { - firstColumnHeight = minLayoutHeight + firstColumnHeight = 2 * layoutPadding + title.height + (priceChange?.height ?: 0) + secondColumnHeight = 2 * layoutPadding + (fiatAmount?.height ?: 0) + (cryptoAmount?.height ?: 0) + } + is TokenItemState.Draggable -> { + firstColumnHeight = 2 * layoutPadding + title.height + (cryptoAmount?.height ?: 0) secondColumnHeight = minLayoutHeight } } @@ -536,6 +532,7 @@ private fun calculateLayoutHeight( } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, fontScale = 2f) @Composable private fun Preview_TokenItem_InLight(@PreviewParameter(TokenItemStateProvider::class) state: TokenItemState) { TangemThemePreview(isDark = false) { @@ -679,6 +676,7 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider Date: Thu, 12 Feb 2026 12:46:41 +0300 Subject: [PATCH 051/106] Updated on 2026-08-14 --- .../com/tangem/common/extensions/KNode.kt | 11 ++++++ .../kotlin/com/tangem/tests/BuyTokenTest.kt | 5 ++- .../tangem/tests/swap/SwapTokenScreenTest.kt | 37 ++++++++++++++++--- 3 files changed, 46 insertions(+), 7 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 7c275607c9..004e367549 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 androidx.compose.ui.test.hasText import io.github.kakaocup.compose.node.element.KNode fun KNode.clickWithAssertion() { @@ -7,3 +8,13 @@ fun KNode.clickWithAssertion() { performClick() } +fun KNode.assertTextContainsSafe( + text: String, + substring: Boolean = false, + ignoreCase: Boolean = false, +) { + assert( + hasText(text = text, substring = substring, ignoreCase = ignoreCase) + ) +} + diff --git a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt index a618a5b1dc..3c44cb4154 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt @@ -1,6 +1,7 @@ package com.tangem.tests import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.assertTextContainsSafe import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState @@ -374,7 +375,9 @@ class BuyTokenTest : BaseTestCase() { onSelectProviderBottomSheet { paymentMethodIcon.assertIsDisplayed() } } step("Assert provider name: '$providerNameMercuryo'") { - onSelectProviderBottomSheet { providerName.assertTextContains(providerNameMercuryo) } + onSelectProviderBottomSheet { + providerName.assertTextContainsSafe(text = providerNameMercuryo, substring = true) + } } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt index 60ec5a8154..2048e0fa7e 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt @@ -30,7 +30,7 @@ class SwapTokenScreenTest : BaseTestCase() { @DisplayName("Swap: network fee") @Test fun networkFeeTest() { - val inputAmount = "100" + val inputAmount = "400" val tokenTitle = "Polygon" setupHooks().run { @@ -99,22 +99,31 @@ class SwapTokenScreenTest : BaseTestCase() { } } step("Assert receive amount is not equal to '0'") { - onSwapTokenScreen { receiveAmount.assert(!hasText("0")) } + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + waitForIdle() + receiveAmount.assert(!hasText("0")) + } + } } } } + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) @AllureId("3549") @DisplayName("Swap: network error test") @Test fun networkErrorSwapTest() { + val tokenTitle = "Polygon" + setupHooks( additionalAfterSection = { enableWiFi() enableMobileData() } ).run { - val tokenTitle = "Polygon" step("Open 'Main Screen'") { openMainScreen() @@ -142,7 +151,12 @@ class SwapTokenScreenTest : BaseTestCase() { onSwapTokenScreen { title.assertIsDisplayed() } } step("Assert error notification title is displayed") { - onSwapTokenScreen { errorNotificationTitle.assertIsDisplayed() } + onSwapTokenScreen { + waitForIdle() + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + errorNotificationTitle.assertIsDisplayed() + } + } } step("Assert error notification text is displayed") { onSwapTokenScreen { errorNotificationText.assertIsDisplayed() } @@ -160,7 +174,8 @@ class SwapTokenScreenTest : BaseTestCase() { @DisplayName("Swap: change network fee") @Test fun changeNetworkFeeTest() { - val inputAmount = "100" + val inputAmount = "400" + setupHooks().run { val tokenTitle = "Polygon" @@ -209,10 +224,20 @@ class SwapTokenScreenTest : BaseTestCase() { step("Assert input amount = '$inputAmount'") { onSwapTokenScreen { textInput.assertTextEquals(inputAmount) } } + step("Assert 'Network fee' block is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + networkFeeBlock.assertIsDisplayed() + } + } + } + step("Assert 'Swap' button is enabled") { + onSwapTokenScreen { swapButton.assertIsEnabled() } + } step("Click on 'Network fee' block") { onSwapTokenScreen { flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { - selectFeeIcon.clickWithAssertion() + selectFeeIcon.performClick() } } } From 299117f8c493042ae3653fb6e320e5cfb3ef7f99 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 16:24:37 +0500 Subject: [PATCH 052/106] Updated on 2026-08-14 --- .../local/preferences/PreferencesKeys.kt | 12 +++ .../drawable-hdpi/img_tangem_wallet_72.webp | Bin 0 -> 1850 bytes .../drawable-mdpi/img_tangem_wallet_72.webp | Bin 0 -> 1196 bytes .../drawable-xhdpi/img_tangem_wallet_72.webp | Bin 0 -> 2494 bytes .../drawable-xxhdpi/img_tangem_wallet_72.webp | Bin 0 -> 3978 bytes .../img_tangem_wallet_72.webp | Bin 0 -> 5266 bytes .../hotwallet/DefaultHotWalletRepository.kt | 76 ++++++++++++++++++ .../repository/HotWalletRepository.kt | 20 +++++ .../intents/WalletWarningsClickIntents.kt | 34 ++++++++ .../utils/WalletWarningsAnalyticsSender.kt | 34 ++++---- .../domain/GetMultiWalletWarningsFactory.kt | 65 +++++++++++++++ .../wallet/state/model/WalletNotification.kt | 18 +++++ .../components/common/WalletNotifications.kt | 9 +++ 13 files changed, 252 insertions(+), 16 deletions(-) create mode 100644 core/ui/src/main/res/drawable-hdpi/img_tangem_wallet_72.webp create mode 100644 core/ui/src/main/res/drawable-mdpi/img_tangem_wallet_72.webp create mode 100644 core/ui/src/main/res/drawable-xhdpi/img_tangem_wallet_72.webp create mode 100644 core/ui/src/main/res/drawable-xxhdpi/img_tangem_wallet_72.webp create mode 100644 core/ui/src/main/res/drawable-xxxhdpi/img_tangem_wallet_72.webp 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 544a55357f..eae141d861 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 @@ -132,6 +132,18 @@ object PreferencesKeys { val ACCESS_CODE_SKIPPED_STATES_KEY by lazy { stringPreferencesKey(name = "accessCodeSkippedStates") } + val SHOULD_SHOW_UPGRADE_BANNER_KEY by lazy { stringPreferencesKey(name = "shouldShowUpgradeBanner") } + + val SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY by lazy { + stringPreferencesKey(name = "shouldShowNextTimeUpgradeBanner") + } + + val UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY by lazy { stringPreferencesKey(name = "upgradeBannerClosureTimestamp") } + + val WALLET_CREATION_TIMESTAMP_KEY by lazy { stringPreferencesKey(name = "walletCreationTimestamp") } + + val HAS_HAD_FIRST_TOP_UP_KEY by lazy { stringPreferencesKey(name = "hasHadFirstTopUp") } + // region Notifications val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") } diff --git a/core/ui/src/main/res/drawable-hdpi/img_tangem_wallet_72.webp b/core/ui/src/main/res/drawable-hdpi/img_tangem_wallet_72.webp new file mode 100644 index 0000000000000000000000000000000000000000..a82e95d6ad26c873a357ff44c33fd8e3cbba1525 GIT binary patch literal 1850 zcmV-A2gUeONk&F82LJ$9MM6+kP&il$0000G0000`002+`06|PpNGSsV00E$NZIdAh z`XaI-YdmY)wsB|Mwr$(CZQ}>nw!LH9(-jw8nNvNP%d3c(0Q~QfI1x*6AWp=JA)0rM zBX8Os6cTYFRt%$hwPT+6ZYb08*AotbI1wv`xpD2&pZQ@ZQ~xvVtRgm&O2Rbn+6UeL z^*~7r9WgvR$Vh4vM%8jhJn&_INh`Xhtq>WpR+#Em+v&ECdZu+YzAi-(Ct`((7TN6j z_d2I^HrJV%5GOVjCbPhfSHIaZt^w|E{kWRbg*Q6=gaxMYDy9A#b1Q*me$a=<C~g?4xneb;KnLMB;7hVxEXY)@U5C_ryW>v@V%160PE59*=`AEDqQ^BXPZl%oB+b zVF&YA9CQtFz+M}P*R4zDjznhI!Db{5x^3!!y*Lt0;*y$3=zNKTOKn@CO`mNJfw%}CYl?0XTXw!rU z{*-itr0=3g*)NEe$6w9S?F!*pG1TGLcUBeN%sZ_Jl9tL$e0%*S4LDGJM{a8bwkqYF zUIb*%qNbak+I1d)rAkgv-w)RUfaf*;x?-q+!8cD_M!=Fv%ngN&pJ5%L%RMcsXepnS2P)VWC^Fm%7 zIetlt0l@QEu%y-y3fw+(b#08?)k^Ao1jUmfLcw!Pt8;h<-Gn={9!>UkkTZ0vCKEs65QKU2@w5#)MPjzdts z-?V!Eu}Bz4X#RJ#W#><-=foZe^F+sC09JcIz0X6S$K~}B<~X58y>FsW_Q<<;iRXIN z{{f3o>9?FFo#|~B;a;0F)2Ra)0WZ>2K2Jh#lk9VTrTZD@HR^x*-r*n0Kdt}K^_}(~`&I0v`w(DQ zLA}n?@NJ^K)aIG`ndda&DQr06Bz`E*siG=c0vs>nfTfg9Y?kZFEKLts z0092}pH${M2PGQ4V;^J-AM~E{jLY=YY3!3$O^Wm}8$aYaPAVx6fdR$rbtso4ljp?u zlcLr7xs+lXHs?kfDFEGndkCe=#EQu*YfQ{gRgGX~MSSmZMf_L&T}NlX-C6vfCKA8H z|D<0aD&3pRgc=9R*f(&)8~$hf*#)ug`7JzDq7k*4N$MD+@jywQP9e^m|1FIfT5ItT zJwA`bCk_&$o|oQ+!*?hAoe7|@XfFi?tm7$u9=H$Rg2KP{qr1<=@WDVN&t1Fa56g7RBOv$mcWa;W zS<7iNn@Fm?8lrh`n4$Bf3QAJ`c>6e1nuGiE6cLxVskBcq8xOGi(%OdIUA$dr!C_y( zmcaP*8XARn>Xm#|w4Y!AnaW!wC%(V}ot1y3F~;1z{Nw~ionde7F0o_#LP1_|%m*_L z%)b?=1a%In@=L>nANz5*cgo!#NT#y9XB5q&>9TF??7x8sRbC_~`IBNf{vJnlsx;|O zZ^^Dzvk&Q&6yiR(ULyv_@*f#R4aqLvNZ8J~GH}BF_>^O2o2F~Hl7(WTR4!7Xod5Rku`mH5>3pX+?OGH-pj?PJZYynYs+1#q!h` zoJ`s))I4gnZ(y`j6oHVe(gAp$+oynG=!V#I!nQ_zYgB2*Cp z_%R+@_zNX-Jdt<)w|ck)V<_6^D{_>77`(Tw$HayHhV`f-!7ST-;&b3otT^d*wdt3n zh-|b;FhJvD^R_>=qaa)Ft=GHUDCIg?DQui-|L&S}4BC>0$x@J?x(*%B*}-I$<@&8( oMcbsjz}2WEQO+mP6G`8Vf|oM%t9iLTKP2(T>ap|zOaK4?0FpVGzyJUM literal 0 HcmV?d00001 diff --git a/core/ui/src/main/res/drawable-mdpi/img_tangem_wallet_72.webp b/core/ui/src/main/res/drawable-mdpi/img_tangem_wallet_72.webp new file mode 100644 index 0000000000000000000000000000000000000000..bdfc895e3c0dd86d8f6220848226f17decf3dbc0 GIT binary patch literal 1196 zcmV;d1XKG`Nk&Gb1ONb6MM6+kP&il$0000G0000r001=r06|PpNb>;z00EE#0FWU` z)iu*S_qJ`@w*4RbL;q)1X4|%{*tTt+eLEF&&7xjuL`(qu&nwiPaZrg-=z7M%+~o(a zxRvPB35=CO*Ju~9{E(Hm6MYIE>P1VTYmAFnZpiZM@ga)%r;bcH+Gtc|`!BlkGel7@ z=>#c9dm2^A?sG4E$);Fujtg*{Mq>`Y%^;yUP5>m5=uVoONU|Rqu7fbjqZpJn&ST7d zVF`xOZG}-8v=mC)9cO{LFD%DlbQ@vROg@Ddv`| zAWU6UK*p9;L_{qhYz##u@#3hKuepp>h2f?(Iy|#Jf{a_!YPo=>>T6=3HU((!TYbZG z^8|!GZs?XDPEYvTZ52H77Wax}(O z5OxEf*`WboSUfW?3Q>Gc_SHN9P%%Vya&ZB@$$ni104lyBb`}WS#S8|3ey@qWg%TcF z2LNEaPwcmfAn~Ks0P3G2KB)nSd_hDH>u-)H{(DD|xr&GqDfo3m5Lzgn!hKK<#E#Am zN?lt31P}Vo_$M2H?2?mDT)8-ok z8(z%@H^cY{(?RkruK574zq=#;`ao%Cn$SwU6$Z_S4C_I7Z;YcIDNEC2xh z{>;8SlbmhH|HL3Heu^u)MZQlJ6w6s6xtULqA?UwmYQaAEyc_eFMT!{Qb<0iu8Hf0e z8CYvQk4QV}E-#_;=ED2sQ(wF1_{4s4vo#5guKyivGNzbza7uY!l-G@-k(iG<@fXKv z_o7Ki-o4$aSiP;*{-=gNsx*Re>UDbKzjxAlNMmB>v1LYC zPG~JOW^RwL_#m=hRx;U>?R=ZYm>7j8-F>WA7T0VXG%4@MYa3n}yB<)?_AF zp(U)_K}~&0dfXGP^>$QX*1`el${wtEO;?)kutIWP{lYs=11 zO{YfLituqqh0F@a{!au@PbaQmEnr$)l>_t@oH=vk64ro^WIw9&E2vcBzU?1^vgE`#ZA?$ Kp=k?7zyJX1g(`Ib literal 0 HcmV?d00001 diff --git a/core/ui/src/main/res/drawable-xhdpi/img_tangem_wallet_72.webp b/core/ui/src/main/res/drawable-xhdpi/img_tangem_wallet_72.webp new file mode 100644 index 0000000000000000000000000000000000000000..31d50c1c29889531c6626ded6c2e1effc1ea3cd3 GIT binary patch literal 2494 zcmV;v2|@N!Nk&Gt2><|BMM6+kP&il$0000G0001M003(M06|PpNFf9O00E!`0FWU` zij0Wt?lJbTwlO%{_Wf+zwr$(CZ5wObHhQ|^&=pzL6X#DvOaS~J+qpjlhs4ap-ZqEC z-ZN&}ZhQ7SQ&tq_-au%OLt@X2)pdKG@z#{(q8#GU5+wGXG1qpx^WL4jToSyLOmM{P z8LO*zJL$CvOC|ZJ3R{&U^2{!`+IFWrH)d%hsqg4pha>XLSYE!-36Bk197(B;;SzI5 z%#6Nn&@qn=URaP)uc8uhNX$%SndW_O`+i=Fw9+e5b4ctxb7hacZuxR^)_;&yMGRFl~j{*o~7w?-_H|+aCY=_@!d%dr4$*gGYugip1ck7K2>VDlvOo4q7b| zyT^yUCziLR09qWLZJKP$IzHOAp%g&2$!eqm zo@v{atfRwho9~^DY?B670Z+H>B?{QzwifSHgto~ZUIl)#ZG&0IyW1u?R1q3?2CKl2 zv28&{$J^T`?O8==-2J2i{>8SHIn-@xn{3Z2LgPLq74VO?NfqkWH|{8}LNsm?tAM|? zZ7Bsb+BOmNNVe?|0Q8<^H$W)&wyH4msdnf>kPMpt;gQ|EL!WgJOmCa>OIOZ#bdad; zVU8RF$s^e_rZzd?H;kD1$>5k7%_;a!CVYM58u(RAbYu!hrIjCd?{?RRjoym@2IXVq z{S<&-DwTP!AKD2W@k0{2;Np4!fEOkHrBphmxA^Nb8p<8AXL4|-gnro_{@9RLe31rn z8ZG1R->Mpry(j13k9S7_^G>dSqR)!*g*1TgNM_#42Xr7u-ZMH-Hm(x=gn;syYR)<243mSuZ#8~h`&(u(F4wgfh#rNSI1o_f#a&d#xq2@unCF}`d$KCb^;^c)m#h38|O&q zsbJ-YifL_&)Sf#}0t+wYtf-l-xE%i2@JS@0Usr<}!{e4W0YH}%3KBf59_;)y?o9wV zew>7V)D8^2F7Bi*=gyG?KkWsUZXLx8o-0YdS_`JuPL4@5ddP#ZM@Pvmy9vy7UnMqa zdoXyT1qpr98BE?~VG;|@Dg&dpniBSCZ?L=lySmwLY~;Xlx5t;Oise5zwL=!H$DX@f zaKTNt9oGl`TMYnKP&goN1pokW8vvaFDr^9306vjIno6aksHvkd9Q(se(@1$_91{TX<7L#3B_qSs&)QZnkFA6G|O{ec8ul;WdYfJ zXv{b6Ln4q@=dlwfHI-WPqe%mWs}7|g8FNyKlv5+HYHk#P($N3_{>Wf_`|~^6Hw`K*NETT!L*Yx}a=LlvpsQ&mrv8@`T8S7v27?|b+CeONblyj1I* zqR=7rYC2KDXVNKYHp{KrLCW?KN(qp$bu}Q@>~#)pO%;6_9=tXSDulIoQ0KFtf|g#@ z+`B*~3j-e_9nHdkLb6Hry~vz+E7bAfuUgmmbG(qj3i48h)w!FMd~W`5jn5 z*ERAwNz|_4M%CiP*Kxq;GlL@$P+5k_8Ry&Jx+;m-tRRQ%YDe$#JAM?Oq~h&y!!vox zjw)`Wlaz6{LZee429MYS5<=w_A`yUt8>g}r!@1v0?(BfDkelpCMDMX|^OvZm(}nmB zjXf#syA7CWrkDbjrgWV4M^dBB*oY5_6ak6!+@x&MCiLtEN{ee{uCO516~gK^QBHXQ zKd$+G(tER%ELZxPuie`=(iS!O!Y5`d1dNF)j84^D!|5OGP6`wUz{^60+g>-?7j1ko z3%m}uWf$cM`4#nSvHSjFUavcR{O@uHe*2gI;iq?SvDNCvFZ(_MBno1k5jdFhi2t=_ z(w&zJqy#o9`H>8fgiKb&D^}BTnV;#r=aIrwU+AfETZ?$COAwD)h-zD(A12XnI}$Cw z)~LxQ?*)zG3+69=#z1U@Xc6Ul9xuRc3N_gFXeOYJ{u>aGawv5vI~Bv_x!pFaZoeoz zDL(7RbR(;6<-HJq<&?zN_#&3zp|X&hgqB0HB5c4p+z2 zCv+)JUzm{yMvuLU&e*+=t!1$@rx%nw8*U(OBh$({NOBuSmhmA&H~&VPnqg#r{axJv zze&Y=!e7CamV+BhXhC0f?Ik9CS9E!+H8+5en{fT?Z{6l?dRN1edwP+BMKeG3Uua=l zk$4;^^y6sh1%Qr!=Zn2p`G>+?CD_oaf&WQLqUIu zlDRk`<}1?J@Is{xXh06OU^eUm@I2NW_7@_0kI&6SwzMA5izo{!PJO!UewlBn*hKpy zb|X8yncPrBHlAq)hbkF==9f8M9@-Q{VsC95tTy#~n2LU=sC0}QL-&TjJFes)4CSF% z$80J@egt`u8uq)7$LZ&OGz?(c_1C|ar#mmBj016I6^Ri_QKxC5V8nE?3U2?LwLtd< zt6gn-c}uk7dhgZ#+0mEmHh4PhTh}Kc4}7B`Sk3GsOhoD|Rl`yq$Ez<{-~HuBVrQxo zt<31Ro|yC$h!oY#4%EA4C0#*bG1>8sKKj3QN73CPk5fdyd;Q)LXGZN7=btQSsWSqq zm16bRr$EJIy&^uo63b7;OG}5=seu2~_3*Bg`~Q$9Hm@tLGrwf&W&waAzYv@0_^ZGo z4ip`@hyCNK1!|cn7HKu+#yjm$U(`W7k)-u1CDHUJ0RM<4%_<8u){EA1a*5w1jB@L6 zgfyf!20ZEddYfVE2l?6raN=cN5RIvt0YDxnrp8(xlyYxJWyP}GCz~(OD^y0{3kvAM I^Nauh01<-EQvd(} literal 0 HcmV?d00001 diff --git a/core/ui/src/main/res/drawable-xxhdpi/img_tangem_wallet_72.webp b/core/ui/src/main/res/drawable-xxhdpi/img_tangem_wallet_72.webp new file mode 100644 index 0000000000000000000000000000000000000000..0fcc24fc07f71fdfaf51b03f350e8eec1761278e GIT binary patch literal 3978 zcmV;54|VWTNk&G34*&pHMM6+kP&il$0000G0001?005x?06|PpNNolH00E$f|Nq%Y z`YIiJ_Bgkbvu)e9ZQJUeZQHhO+n&wY*RBVh_IsM-o3y{*U-#S-5fcFa&k~8qTM$`< zg+=NE??5CG7N($n+LaHv?V~^X3vX`Y3rNHwA}n=1%j|yLyT!7`e6@|-hR7@|%#@pE z=|gV+aN@8UevxBGAu+Qs=Nfv}Jot`}3nfkQrhwgoNW#L*LH$$<>~`G;zYl8$d&Ct; z%)-npoeS)F`D?!qX?D*is&z;#BFwq^?!~q|_thT;H1qvUQnQelg++*4X5HlMm%bgW zXtgd)wFikW};E^?sM~p#j@7)nI&6@ zEX=~x*tz%~*S+=IfV%JsqHG`%3$swJX@QMSd*Qpmin?>KS<*scVPU4`={G#-;lBQg zx_3EB1?vLN)z7%%{x^O)uB^^}Qz(TTwQcj_@$H8W?!6iLHHy|6H&L`dyG6|&Q5!x;$#1Hz!L1Y3nhfn<4$R=8Pfm=NFQ6W0=10L|nRgN7k=yIRX`Pc$@DzA8o$U4U>fg<9$Lq!~$uISDcF$K%HJODaRH;Fu^BuQL#We&MO`!G5~J(ic*_c zNKo_%-3%>|j`E6#$pE;;D=N)_g~-o7p=Y24(0*P~2-rdzyh0Zd3l4triaQ7mfOhkW z!Dg|LX0OoI&_sUlidTsYkhb-TazkJM(&QC-C|R)l;1%Bs8z61&6*{vRfYkfNDQLm+ zwO4#8XaKa8SNz0aDUDw76qs`GzE7-e1Gtt)e33JfoIm{9fkqU(<_pgff;Fz@0i`eQ z+^`<&z+BjArSI?EXP#ytmYje+%bU)6Z}C<@77-zYba5?b>4pI_B4khkcSY@xQIc1k zrAK82aAkM>ni&z7lwbNXmadQ$l2=?&d2EYzcst(NSpi(k4aE~W03g)(Z)QfMMvQxX z%+}x86_0O+e;%9QkIWFiTy2A2UAx|BL7F*I>){L9;oqYK+LINa#jEJI$M&165r{>Y zXN=VPt+`>uZ8d3_8R2pjREp0Yzf?OQvoI4PKx>~GR}LW2s0r<>UXQ@S0ThdRt=}h z6aXyyROug+KfGl9sTfH_m=PhH`>W~8?GQgK7(4>mpJvIGW^V)%34{r<9F|QU4j|!J zHQb>J0EvYOA@g6T5>STMN?oEer4Cd>~B!$>WQp*yZs!@VG3Z=|MqaAt4G;3P=cJ627jz#XS*@((FN zZ=|;CgF6e!211A*MrwN*xN|*CzCXkdBejjDbI7&H9)bAF71Ve!aOf|I2EuszRn#~S z4qZ$W=OOkvYW*^}bk{`DG;!LWQR`?rr<{>!4~V&Zq}Iim!Kt?Vk_kq` z3Qrd`)jT*jSBy3J?xLog2PdB&EA@#xfSVV{P;nt}^|P6YLU4A^q>Mb+1r9$T6O#@A zw_lKfv9p2W@rBe*Z3XAIznjhri+~TdUY|l?Kk&u8RWaWV{pm6pV3}X(bd^W{<7)+v&Pw==d|9Uc1qYO1`Rjn|Cy=WN zjbe?FRY?TJs)klRE?ivgjR_wB-5a_Ha0XY9Ify*o#;4uS5ROA@v#p6*y^)lHeMyH0 zHz;>1_QT!8hLI?u1;qH+P}X3286xpb%u*nB$^OJ;2M*kWhfUZ|+(y=mKl*_)5Jk_} z=M7j>Pbf8q&Lc|Xy3-SJyM(;*(S!5GMQ-Lr3IL|ck)$CZ2j``$`)7Y8`j}&O8z~+x z4cM)rR7HMUlxz0>lozN0rff-lr+UCbT~SEjxW7G#5w)-D?$|fRA(w1b7(d&T*0je! z0RH=&!;%A=O9B%HdCF7o%H9Ug-xWmmxgyP)Y+9O23%Kw9I#49?z1C*aQ+dAVWFH&DY3Q976^}%C&--5sT z*NB!<#5(p-M2<5kO4RznR(LE-1`u!m(m(|#Z&v#XB^kD0F^oJ6-zWf-5aUI7z+miH z_?Tb1B4w-&;c9VpHqyQSfx35WJWV95KM}gh(9IXffS6iHZ1Y0pLmVx7JF`LHjaHQi znh+k$tw}IAP7JR= z-MhV83OMg81Yb*gH#gi8Mf*6+Rus|wg$rTaYw&L)`{IG?&{98pB=qkHj=Qy%Z(+OQ zcuySw?2YSaQxVY5c;C>juk#77vjPMcuGVk29mcYe3OQK+s`5-xCO?k?Q{w5_obBpW zoQ4=`%xPAXxsfT7U{p*-g5Z9Pa7U8xZ#8NJc`09_D;QAJ5js?2bpnd2yt^4W6ONK{ zM}8+u)VhX1F-ZksmVb4d6f$Bt&FB!@QHtL>T2fS!4c_vLAOy5r7@mxe^3 zmXK10%uXcTy1V7eJX-v0`)3&^skmaZ5zS<$nIt0I&%L`e@&7Q>o?i(FQ?Pxktyf$pl0pw%r`N9(PyFotj8$#bo(eYhMo2cY zEe8C@sC?cX!G(P1z&G}}E4{ZHnBywZJ*7_ht`{hHprQdj5~pThi*CkhK(hmk@$4T< z20qiij5tAD97Ld7EJbcqA|hl#DuHgcUnZyPc47ikp$+CiOoK{9sp`r3H`GySU6Th> zMrVf`rmzlM5Q6`MJCVQ%%uwQKdg!xLC;yt2-xICJmOTBJwZQ{NdJ`qTNyAQ}B@X|O znt(Iia~L0weLOmkdUp*REPJGOaAOnNCrunSd2hgxKB3L3=+@13-+dWV;wVwO3gYp} zFAiR+myk-G4BzA#@E5@43y2D~xjgpYv%zFQ#}q*o4o{5_R zAenmrb6=p#1KN?>vvXgNXmC+Tbmm>HDMgm;nU=p%nZeBAn)ilF!Tnf_1r!rLt5-(t z&dLkva*N_zVJD8p0otfz*8!bgi{Jp1uy=qrVU%YFtB0`n8l45?)`(RBZRlD<=$)^* zUQgPZ-b+q82#hLA>q`z{KwEB_CaB+^aF>-i4V;**U5udA$-frFh3^#mfSL5vN7Scs zF|0MRGA7}#K;fK4L|5BExF-0Lzz|E~Gj>7f2MY)6Sf7t;Jh1HXoLiQR!V?Vac@Zp! zKN8QgzL;doV*aTA{GdPnO^5&TPv{6)v_BaF9;xVi2{QE1LSGLxqhM@^8!_T%EgJl{ z-N0OlTj5S%Z~e6-U%7sZCG^Wu0{oSN&DTYU%{%I~ z%M1JA20ZoJrAGb5qel7=w|4GO287CnY}18k3qN)AXE~iyTT`^)Hw`(KzEtCEM;Uy3 z+xCs|9W!o@r0rp4AGVF+u+6$XY#YU4+s9EHwr$&HvU{Lzvuh<;<6!n2Ywe8wcb*Y3 z0rY?C^MVIM1dsq;U|@&+xu`O`-~w0 zKvngo``-M~#1@S?c477vLj*vlPMZ$A{-en)YB{M|e8hkNsHt7|fEzxT)S_v8o!K`G z2!P7kb^2cX)`a<*<_&^V7!UwuHLL7$!OLUjY6ib1Z!#bPRMf1r`{3t(o24195ic+x z0)*<^R%br@%Ph_QP;_|A5CO`n7vBDuM}MA?Xo*Z_fIBXR1OQcwc01~U@20hBsa-L2 zKnw_g>gpZ(-2K(mHZ9SQaYw=s0Z_Hj#si0cQfSrE-dc{1fB^weS+(_^!#|tUq8(#5 zDQOG{fQs7H_PzYQiF36hO}?kn%zy|G%Gc;N_=Wsz?T{Ch(!vZ8AXG20?HNz~IxEo* z{8TO#3=yEbJh#Dd4}3SRO*{N#1|e$<2q2XUZ+ggxZwqbe$IU`!7!W}y>%7{&*S|Bd zMcw%lr;k}gfU-_&bi3%q-{+}U+kg}?M1WAf#KwakZkm;-b9+fzB#11%(usF{HN8z; zoa8B;TwZnF&-2yST_p{|!xDA(Wk>^mtqz~boPw{_<$D3Bhl=id1p7MfPBsZ}+}tu)5dGvg?I9+ztK&Xk8+M#l z!Gh=;$DJ%DvZLcB1~y^G>0)9*^r7QM!6aJ^MozJYU=n6#R8l-?hRNFJ?Xf%3Rr}d z9H;w<1=w?=$@X!_>0)9Lmi65EYyoa@+$?4TqDLL4I|K{KdG2Yp0JS=<6)+&a#c?_) zScFAA_ZwM)<~mMm#DMq~&s}U17WLc=W&_Y{$LY$!fcP5EJya^zRHyeR>rmmd z)*j4OOyd%V-HE_tj{fTEmS6AfzcfOi6b5nA#3v71sZsBT_xZd*@4gqhMF%G{%kb(GODiFfqejG(u=w@{yr5d5R(W`k%<;+}~a!GY?{$#aX+ zowFfu#r)S#Se2TJyCNGzjfoq+Kd}oDZKlQlkP+Eko%s3=^shHW&}A}$QaAO@(}t(v zwQlsEO))ml2GLJRshai8#aq`yFjVc|-j@Emm&WXs5!ppcZ}P{(2QC5mCwQ=$7mla) zz8dB1te~_^d+X%Yt3?0-(?8Eu^IJMod$-i6=VyfI`=m6?{QQ!w>mh&uO#gkXT2J1G z+CMVRC0UW(HR~<;yLMR=0ucdIk-H^UcXp=s=QQs5nL(DCUl_Pdg$N*EikX|DwsVfB zVqc1MLRN^rPqHxg$E&vM0s#cf6nojc+WxXU728`Q_sC4xQq$e?+nxI@Qzj4*Fxh^r zwvW|Q(fL@bX9hGoNlWwVr>s#W0*ElBaERKTL&cw?k;hVYv4z^t{_gTE7ZL#k%#_mp zYC2>ynVAi9kzb02q0jlYz|S&@0X&8xhc^;pv;>X=#?nyr)oIB6}9cT(|nkE!u>)mCuWoRgn*Utqkej0XjCQT_ncC-b3%VG|F<5mYbu-9 zBB!n#VP4HYdcLmdEUZUf#pL3iC+abzSyMV?UGnRF#l3h(qo#KJTIATRi%WCUO61w< zLUz7bf_ytTGmq4gbGsHYasm1Gq)Zg{A`icjfyR}{#Vb!s-SE!juqB#$39(`>>Za(XUrGwHZ`a(g)6;Nt;x@V0FXcc zodGJ?0N4ONkwlzJrK6&uq7hp_uo4MrZu~@hutxDFFA0U1scI zy4*X)D|q8`Y?b+wC9*fAcFWQ~gV8EIBrc}`xFdu$-QO-GEQ@RjTOA##U~p72i~Ozc zrBuU?N?vf~sehvg}&HX}5`i=0>L&X84Y5H>+k))(wfT2igW)Hu7?^1^-(o`)Cq z5%_EJJy`PYcjr5KC&rVXs^tsx+*`pUvMbBlr>&N?6r+Z?(J~p~ zI+#_L1^1ekAv)?%14q%Hpf}I zEs_OvpPVJfKpWsI`~`Hw=V`gc3lrfQSnIQ(6^IKcFqddB3A5r!Sr6mmKulrn0CJ3P zt}bkP9=sppk=Z&)ZeW;&EEVT@|IV_92c!bG>LpxeV0HKxeMWomjF;+hjVP-cq{2~2w4~xPBMW2vQ#{Y===jyV(xpF!47pQgs}f*RVhp3Hqw_E)1I5@`s!Mym|7?V286$!>OJ zGwWfSuA_O*an_~>S%gLv2pEOs>J**DR6F_AO?9QI7d!rK9THbvsp$zjS0EEop;w@+ z^hK6hefHDJ?KubYY(;+^gi@(E6!x~Fi=Q-u{=-I@cdM~&mOF3ZttiD(x}c|2aLxDv zL*p=2ICB_i>BR-pKQraoNG{!dPK_QJ^97PAoPtj=@lzq`{c&;Vhgc#0ujTW2=gHpk zb+6my<^QW~l=e$J^lH0l(PAM#|6$2aGsVFo59E<7k}T*Gu-^k|ge_y6xj>jsgu|&y z4QM&bs0>BvhOjPh&W);!Ra7-EgLmCa?t!of87{2duk3q!NkVYgj4R~3Va`qBD%_i9 z5j9N?hvJ4YNa3-0uSY67R$>?{h4i|qgippp$1bf8u&h+izzNW679#PC@k`d6v5Wxh zROpcEbs^MW8Amfoe{KeNKl+_@{@h>yDt1PMyed7X2mgvDo3@3k-diO2Td%@A|Ixb~ zn|xqk*Mjy~HoFS}y+y_#msae*-H|J5B==Nc_kiSxQqjli#?o!3SyE0*5nRBw)-0_R zV7V(-`qy&nU-%m*=$}qK zf>S0RX~|-8;oIUBv{dJ|MY)2=GGhoSa|Z*8qqA5Y_Y;w$G6d7zjnF{yi%kJT@!>+~ z3Wk>6?(fVFe>NJS8q|j}%;91B5y}Z?U-zX+hSg5&8g9{$y{`l8AFa$X2?RNySw5{l zf!g)LEtC7S!el-Wq+R3)40a{me+4_|%?tKV(cX3BPM=TePLFw#cSm`l#tjyx;&I{I zv*qu1J_$NEE7h)$vk@ECbbHxrH+tf!mqdj3BTe(K|9vTqBWOpj05MWMQhy7fIH;?k z*#xx4ygiq`uC9ZkEm9+<+FNQgG>Dc(ZS z>5Bc&PBADrZbVpAJ_G82?(9ZDUi57x&HKSmP`N+WZ#$;BzyG5zUypn%4TXq7QK49F zTW1Q8ut%y!Dn%=DBhv?PSt02|fCF;qx6dT?11ea`Ne5p)qVyv3o6?G#R87-Hz8-5u z29#*92CL%)bb-HDje28A)@H!q?aAlo>^9aa8VrHOu7ArTX0Oiu09#gU@`eY-F6Dwn zy&agp`iv(lsoO*EblY?cyY^S?ku`H?=3H&H9#Rk@gC%;WY1Fs(HHZr@Btiz_!b}8(Ffa`FM6a=zs1*GLaHBbLAYEU&X4>y-F za`H#yJ1HH&w#f|PqQSC1%Ed^~|8h4a>;4F+An~AZVW68S!_1)-D2rS^V6s~4`!nf`2NyG)1v?IO&yIGd|2=vDfaiBUQpl+)`y0cWyW`{tS9;!~k?; z?|2hN%A5xEO||RcC+b+u{pT9>B$&&CDp^ckRlB697n-=`Pl|{stsE-?)U@I z3)d7Qpz2aRsPc5KdM#bnY4E;3TkP+k0UP3cqERXHEuNq7k6o6=e3VnZsMW`=S|e|Z!d+i5OwyYZRdgt`WA|18V8z6}ao1r4 zDz2TMouiBnG4wUk10J@5m_eW!|5k-wIpUD(u7qmamL^muE6SqPz1Any!%=Khs*wy%62*pk`xpArUf9{v`e{aH%b`{3W*B{6h@9+xG{W1H?|NN~jIXlDW zlD1q4*zx~XvI!0ktS>3#%R02x+3EZ04*H(0K}vB(%sH#vFC*^Q){T4x = appPreferencesStore + .getObjectMap(PreferencesKeys.SHOULD_SHOW_UPGRADE_BANNER_KEY) + .map { it[userWalletId.stringValue] == true } + + override suspend fun setShouldShowUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) { + appPreferencesStore.editData { mutablePreferences -> + mutablePreferences.setObjectMap( + key = PreferencesKeys.SHOULD_SHOW_UPGRADE_BANNER_KEY, + value = mutablePreferences.getObjectMap(PreferencesKeys.SHOULD_SHOW_UPGRADE_BANNER_KEY) + .plus(userWalletId.stringValue to shouldShow), + ) + } + } + + override fun shouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId): Flow = appPreferencesStore + .getObjectMap(PreferencesKeys.SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY) + .map { it[userWalletId.stringValue] == true } + + override suspend fun setShouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) { + appPreferencesStore.editData { mutablePreferences -> + mutablePreferences.setObjectMap( + key = PreferencesKeys.SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY, + value = mutablePreferences.getObjectMap( + PreferencesKeys.SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY, + ) + .plus(userWalletId.stringValue to shouldShow), + ) + } + } + + override suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long? { + return appPreferencesStore + .getObjectMapSync(PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY)[userWalletId.stringValue] + } + + override suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long) { + appPreferencesStore.editData { mutablePreferences -> + mutablePreferences.setObjectMap( + key = PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY, + value = mutablePreferences.getObjectMap(PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY) + .plus(userWalletId.stringValue to timestamp), + ) + } + } + + override suspend fun getWalletCreationTimestamp(userWalletId: UserWalletId): Long? { + return appPreferencesStore + .getObjectMapSync(PreferencesKeys.WALLET_CREATION_TIMESTAMP_KEY)[userWalletId.stringValue] + } + + override suspend fun setWalletCreationTimestamp(userWalletId: UserWalletId, timestamp: Long) { + appPreferencesStore.editData { mutablePreferences -> + mutablePreferences.setObjectMap( + key = PreferencesKeys.WALLET_CREATION_TIMESTAMP_KEY, + value = mutablePreferences.getObjectMap(PreferencesKeys.WALLET_CREATION_TIMESTAMP_KEY) + .plus(userWalletId.stringValue to timestamp), + ) + } + } + + override suspend fun hasHadFirstTopUp(userWalletId: UserWalletId): Boolean { + return appPreferencesStore + .getObjectMapSync(PreferencesKeys.HAS_HAD_FIRST_TOP_UP_KEY)[userWalletId.stringValue] == true + } + + override suspend fun setHasHadFirstTopUp(userWalletId: UserWalletId, hasTopUp: Boolean) { + appPreferencesStore.editData { mutablePreferences -> + mutablePreferences.setObjectMap( + key = PreferencesKeys.HAS_HAD_FIRST_TOP_UP_KEY, + value = mutablePreferences.getObjectMap(PreferencesKeys.HAS_HAD_FIRST_TOP_UP_KEY) + .plus(userWalletId.stringValue to hasTopUp), + ) + } + } } \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt index e228cd4419..2432bc3dc9 100644 --- a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt @@ -12,4 +12,24 @@ interface HotWalletRepository { fun accessCodeSkipped(userWalletId: UserWalletId): Flow suspend fun setAccessCodeSkipped(userWalletId: UserWalletId, skipped: Boolean) + + fun shouldShowUpgradeBanner(userWalletId: UserWalletId): Flow + + suspend fun setShouldShowUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) + + fun shouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId): Flow + + suspend fun setShouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) + + suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long? + + suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long) + + suspend fun getWalletCreationTimestamp(userWalletId: UserWalletId): Long? + + suspend fun setWalletCreationTimestamp(userWalletId: UserWalletId, timestamp: Long) + + suspend fun hasHadFirstTopUp(userWalletId: UserWalletId): Boolean + + suspend fun setHasHadFirstTopUp(userWalletId: UserWalletId, hasTopUp: Boolean) } \ 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 0dd7d1af6f..ca5a2d4caa 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 @@ -17,6 +17,7 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.hotwallet.repository.HotWalletRepository import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -98,6 +99,10 @@ internal interface WalletWarningsClickIntents { fun onFinishWalletActivationClick(isBackupExists: Boolean) fun onYieldPromoTermsAndConditionsClick() + + fun onUpgradeHotWalletClick(userWalletId: UserWalletId) + + fun onCloseUpgradeBannerClick(userWalletId: UserWalletId) } @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @@ -130,6 +135,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase, private val uiMessageSender: UiMessageSender, private val reviewManager: ReviewManager, + private val hotWalletRepository: HotWalletRepository, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { @@ -544,6 +550,34 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( urlOpener.openUrl(YIELD_PROMO_TERMS_LINK) } + override fun onUpgradeHotWalletClick(userWalletId: UserWalletId) { + modelScope.launch(dispatchers.main) { + val userWallet = getUserWalletUseCase(userWalletId).getOrNull() + if (userWallet is UserWallet.Hot) { + hotWalletRepository.setShouldShowUpgradeBanner(userWalletId, false) + hotWalletRepository.setShouldShowNextTimeUpgradeBanner(userWalletId, false) + appRouter.push(UpgradeWallet(userWalletId)) + } + } + } + + override fun onCloseUpgradeBannerClick(userWalletId: UserWalletId) { + modelScope.launch(dispatchers.main) { + val userWallet = getUserWalletUseCase(userWalletId).getOrNull() + if (userWallet is UserWallet.Hot) { + val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(userWalletId) + val currentTime = System.currentTimeMillis() + + if (hasHadFirstTopUp) { + hotWalletRepository.setShouldShowUpgradeBanner(userWalletId, false) + hotWalletRepository.setUpgradeBannerClosureTimestamp(userWalletId, currentTime) + } else { + hotWalletRepository.setShouldShowUpgradeBanner(userWalletId, true) + } + } + } + } + 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/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 3852ada00e..341c2c6726 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 @@ -6,8 +6,9 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.* import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType -import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen.* +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.PushBannerPromo.* import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider @@ -40,16 +41,16 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( @Suppress("CyclomaticComplexMethod") private fun getEvent(warning: WalletNotification): AnalyticsEvent? { return when (warning) { - is WalletNotification.Critical.DevCard -> MainScreen.DevelopmentCard() - is WalletNotification.Critical.FailedCardValidation -> MainScreen.ProductSampleCard() - is WalletNotification.Warning.MissingBackup -> MainScreen.BackupYourWallet() - is WalletNotification.Warning.NumberOfSignedHashesIncorrect -> MainScreen.CardSignedTransactions() - is WalletNotification.Warning.TestNetCard -> MainScreen.TestnetCard() - is WalletNotification.Informational.DemoCard -> MainScreen.DemoCard() - is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses() - is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem() - is WalletNotification.Critical.BackupError -> MainScreen.BackupError() - is WalletNotification.NoteMigration -> MainScreen.NotePromo() + is WalletNotification.Critical.DevCard -> DevelopmentCard() + is WalletNotification.Critical.FailedCardValidation -> ProductSampleCard() + is WalletNotification.Warning.MissingBackup -> BackupYourWallet() + is WalletNotification.Warning.NumberOfSignedHashesIncorrect -> CardSignedTransactions() + is WalletNotification.Warning.TestNetCard -> TestnetCard() + is WalletNotification.Informational.DemoCard -> DemoCard() + is WalletNotification.Informational.MissingAddresses -> MissingAddresses() + is WalletNotification.RateApp -> HowDoYouLikeTangem() + is WalletNotification.Critical.BackupError -> BackupError() + is WalletNotification.NoteMigration -> NotePromo() is WalletNotification.SwapPromo -> NoticePromotionBanner( source = AnalyticsParam.ScreensSources.Main, program = Program.Empty, // Use it on new promo action @@ -92,16 +93,17 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( WalletActivationBannerType.Attention -> AnalyticsParam.EmptyFull.Empty WalletActivationBannerType.Warning -> AnalyticsParam.EmptyFull.Full } - MainScreen.NoticeFinishActivation( + NoticeFinishActivation( activationState = activationState, balanceState = balanceState, ) } - is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport() - is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond() - is WalletNotification.PushNotifications -> WalletScreenAnalyticsEvent.PushBannerPromo.PushBanner() + is WalletNotification.Critical.SeedPhraseNotification -> NoticeSeedPhraseSupport() + is WalletNotification.Critical.SeedPhraseSecondNotification -> NoticeSeedPhraseSupportSecond() + is WalletNotification.PushNotifications -> PushBanner() is WalletNotification.Warning.TangemPayRefreshNeeded -> null - WalletNotification.Warning.TangemPayUnreachable -> null + is WalletNotification.Warning.TangemPayUnreachable -> null + is WalletNotification.UpgradeHotWalletPromo -> null } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 6381c3f6a4..6f8e6b1be6 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 @@ -14,6 +14,7 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase +import com.tangem.domain.hotwallet.repository.HotWalletRepository import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrency @@ -43,6 +44,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map +import java.util.concurrent.TimeUnit import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -58,6 +60,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val notificationsRepository: NotificationsRepository, private val accountDependencies: AccountDependencies, private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, + private val hotWalletRepository: HotWalletRepository, ) { @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod") @@ -98,6 +101,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(), shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.YieldPromo) .distinctUntilChanged(), + hotWalletRepository.shouldShowUpgradeBanner(userWallet.walletId).distinctUntilChanged(), + hotWalletRepository.shouldShowNextTimeUpgradeBanner(userWallet.walletId).distinctUntilChanged(), ) { array -> array } .combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = array) } .map { array -> @@ -111,12 +116,22 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( 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 shouldShowNextTimeUpgradeBanner = array[9] as Boolean buildList { addUsedOutdatedDataNotification(totalFiatBalance) addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) + addUpgradeHotWalletPromoNotification( + userWallet = userWallet, + flattenCurrencies = flattenCurrencies, + clickIntents = clickIntents, + shouldShowUpgradeBanner = shouldShowUpgradeBanner, + shouldShowNextTimeUpgradeBanner = shouldShowNextTimeUpgradeBanner, + ) + addFinishWalletActivationNotification( userWallet = userWallet, flattenCurrencies = flattenCurrencies, @@ -449,7 +464,57 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } + @Suppress("CyclomaticComplexMethod") + private suspend fun MutableList.addUpgradeHotWalletPromoNotification( + userWallet: UserWallet, + flattenCurrencies: Lce>, + clickIntents: WalletClickIntents, + shouldShowUpgradeBanner: Boolean, + shouldShowNextTimeUpgradeBanner: Boolean, + ) { + if (userWallet !is UserWallet.Hot) return + + val currentTime = System.currentTimeMillis() + val creationTimestamp = hotWalletRepository.getWalletCreationTimestamp(userWallet.walletId) + val closureTimestamp = hotWalletRepository.getUpgradeBannerClosureTimestamp(userWallet.walletId) + val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(userWallet.walletId) + + if (creationTimestamp == null) { + hotWalletRepository.setWalletCreationTimestamp(userWallet.walletId, currentTime) + return + } + + val daysSinceCreation = TimeUnit.MILLISECONDS.toDays(currentTime - creationTimestamp) + val daysSinceClosure = closureTimestamp?.let { TimeUnit.MILLISECONDS.toDays(currentTime - it) } + + val currencies = flattenCurrencies.getOrNull(isPartialContentAccepted = true).orEmpty() + val hasBalance = currencies.any { it.value.amount.orZero().isPositive() } + + if (hasBalance && !hasHadFirstTopUp) { + hotWalletRepository.setHasHadFirstTopUp(userWallet.walletId, true) + } + + val shouldShow = when { + shouldShowUpgradeBanner && hasBalance -> true + shouldShowNextTimeUpgradeBanner && daysSinceClosure != null && daysSinceClosure >= UPGRADE_BANNER_RESHOW_DAYS -> true + !shouldShowUpgradeBanner && !shouldShowNextTimeUpgradeBanner && hasHadFirstTopUp && daysSinceCreation >= UPGRADE_BANNER_RESHOW_DAYS -> { + hotWalletRepository.setShouldShowNextTimeUpgradeBanner(userWallet.walletId, true) + true + } + else -> false + } + + addIf( + element = WalletNotification.UpgradeHotWalletPromo( + onLaterClick = { clickIntents.onCloseUpgradeBannerClick(userWallet.walletId) }, + onUpgradeClick = { clickIntents.onUpgradeHotWalletClick(userWallet.walletId) }, + ), + condition = shouldShow, + ) + } + private companion object { const val MAX_REMAINING_SIGNATURES_COUNT = 10 + const val UPGRADE_BANNER_RESHOW_DAYS = 30 } } \ No newline at end of file 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 9abf719a45..ed1601ae6a 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 @@ -445,4 +445,22 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ), ) + + data class UpgradeHotWalletPromo( + val onLaterClick: () -> Unit, + val onUpgradeClick: () -> Unit, + ) : WalletNotification( + config = NotificationConfig( + title = resourceReference(R.string.hw_upgrade_to_cold_banner_title), + subtitle = resourceReference(R.string.hw_upgrade_to_cold_banner_description), + iconResId = R.drawable.img_tangem_wallet_72, + buttonsState = ButtonsState.PairButtonsConfig( + primaryText = resourceReference(R.string.hw_upgrade), + onPrimaryClick = onUpgradeClick, + secondaryText = resourceReference(R.string.common_later), + onSecondaryClick = onLaterClick, + ), + iconSize = 72.dp, + ), + ) } \ No newline at end of file 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 d8b52ad1ad..90fc34c41f 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 @@ -5,6 +5,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier import com.tangem.core.ui.components.notifications.NoteMigrationNotification import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.res.ForceDarkTheme import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import kotlinx.collections.immutable.ImmutableList @@ -40,6 +41,14 @@ internal fun LazyListScope.notifications(configs: ImmutableList { + ForceDarkTheme { + Notification( + config = item.config, + modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), + ) + } + } else -> { Notification( config = item.config, From 184aa49d3ae32bf08a409a48d64fa8970d914dac Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 14:25:07 +0300 Subject: [PATCH 053/106] Updated on 2026-08-14 --- .../v2/send/confirm/model/SendConfirmModel.kt | 6 +----- .../state/StakingStateController.kt | 5 ++++- .../SetButtonsStateTransformer.kt | 21 ++++++------------- .../confirm/model/SendWithSwapConfirmModel.kt | 6 +----- .../feature/swap/ui/SwapScreenContent.kt | 5 +---- .../ui/common/WcTransactionRequestButtons.kt | 6 +----- .../approve/YieldSupplyApproveComponent.kt | 6 +----- .../YieldSupplyStartEarningComponent.kt | 6 +----- .../YieldSupplyStopEarningComponent.kt | 6 +----- 9 files changed, 17 insertions(+), 50 deletions(-) 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 2286922911..3411103f5c 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 @@ -23,7 +23,6 @@ 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.core.ui.extensions.wrappedList import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -626,10 +625,7 @@ internal class SendConfirmModel @Inject constructor( private fun getPrimaryButtonText(confirmUM: ConfirmUM, isHoldToConfirm: Boolean): TextReference { return when { - isHoldToConfirm -> resourceReference( - id = com.tangem.core.ui.R.string.common_hold_to, - formatArgs = wrappedList(resourceReference(R.string.common_send)), - ) + 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) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index e8ee07c6e8..00409eed5c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles import com.tangem.domain.models.wallet.isColdWallet import com.tangem.domain.models.wallet.isHotWallet import javax.inject.Inject @@ -25,6 +26,7 @@ import javax.inject.Inject @ModelScoped internal class StakingStateController @Inject constructor( urlOpener: UrlOpener, + private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles, ) { val value: StakingUiState get() = uiState.value @@ -40,7 +42,8 @@ internal class StakingStateController @Inject constructor( mutableUiState.update { state -> state.copy( showColdWalletInteractionIcon = userWallet.isColdWallet, - shouldShowHoldToConfirmButton = userWallet.isHotWallet, + shouldShowHoldToConfirmButton = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled && + userWallet.isHotWallet, ) } } 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 04a86dbec7..00b3379362 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 @@ -7,7 +7,6 @@ import com.tangem.core.navigation.url.UrlOpener 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 import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.utils.getPendingActionTitle @@ -50,7 +49,7 @@ internal class SetButtonsStateTransformer( val isIconVisible = isConfirmation && !isCompleted && !isHoldToConfirm val isPrimaryButtonDisabled = prevState.isPrimaryButtonDisabled() return NavigationButton( - textReference = prevState.getButtonText(isHoldToConfirm), + textReference = prevState.getButtonText(), iconRes = R.drawable.ic_tangem_24.takeIf { prevState.showColdWalletInteractionIcon }, isDimmed = isPrimaryButtonDisabled, isIconVisible = isIconVisible, @@ -92,7 +91,7 @@ internal class SetButtonsStateTransformer( else -> true } - private fun StakingUiState.getButtonText(isHoldToConfirm: Boolean): TextReference { + private fun StakingUiState.getButtonText(): TextReference { return when (currentStep) { StakingStep.InitialInfo -> { val initialState = initialInfoState as? StakingStates.InitialInfoState.Data @@ -103,7 +102,7 @@ internal class SetButtonsStateTransformer( } } StakingStep.Success -> resourceReference(R.string.common_close) - StakingStep.Confirmation -> getConfirmationButtonText(isHoldToConfirm) + StakingStep.Confirmation -> getConfirmationButtonText() StakingStep.Validators -> resourceReference(R.string.common_continue) StakingStep.Amount, StakingStep.RestakeValidator, @@ -112,7 +111,7 @@ internal class SetButtonsStateTransformer( } } - private fun StakingUiState.getConfirmationButtonText(isHoldToConfirm: Boolean): TextReference { + private fun StakingUiState.getConfirmationButtonText(): TextReference { val confirmationState = confirmationState as? StakingStates.ConfirmationState.Data ?: return resourceReference(R.string.common_close) val amountState = amountState as? AmountState.Data @@ -125,10 +124,8 @@ internal class SetButtonsStateTransformer( } } - val baseText = getBaseActionText(confirmationState) - ?: return resourceReference(R.string.common_close) - - return baseText.wrapWithHoldToIf(isHoldToConfirm) + return getBaseActionText(confirmationState) + ?: resourceReference(R.string.common_close) } private fun StakingUiState.getBaseActionText( @@ -139,12 +136,6 @@ internal class SetButtonsStateTransformer( is StakingActionCommonType.Pending -> confirmationState.pendingAction?.type.getPendingActionTitle() } - private fun TextReference.wrapWithHoldToIf(condition: Boolean): TextReference = if (condition) { - resourceReference(id = R.string.common_hold_to, formatArgs = wrappedList(this)) - } else { - this - } - private fun StakingUiState.onPrimaryClick() { when (currentStep) { StakingStep.InitialInfo -> clickIntents.onNextClick() 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 ca0ed8b981..6fdf6bdb46 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 @@ -21,7 +21,6 @@ import com.tangem.core.decompose.navigation.Router 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.wrappedList import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCase import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.express.models.ExpressProviderType @@ -550,10 +549,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( private fun getPrimaryButtonText(confirmUM: ConfirmUM, isHoldToConfirm: Boolean): TextReference { return when { - isHoldToConfirm -> resourceReference( - id = com.tangem.core.ui.R.string.common_hold_to, - formatArgs = wrappedList(resourceReference(R.string.common_send)), - ) + isHoldToConfirm -> resourceReference(R.string.common_send) confirmUM is ConfirmUM.Success -> resourceReference(R.string.common_close) confirmUM is ConfirmUM.Content && confirmUM.isTransactionInProcess -> resourceReference(R.string.send_sending) 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 e55c8398bf..b4d3c15097 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 @@ -383,10 +383,7 @@ private fun MainButton(state: SwapStateHolder) { state.swapButton.isHoldToConfirm -> { HoldToConfirmButton( modifier = Modifier.fillMaxWidth(), - text = stringResourceSafe( - R.string.common_hold_to, - stringResourceSafe(id = R.string.swapping_swap_action), - ), + text = stringResourceSafe(R.string.swapping_swap_action), enabled = state.swapButton.isEnabled, onConfirm = state.swapButton.onClick, isLoading = state.swapButton.isInProgress, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt index 199cc48016..8f5a202a76 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt @@ -12,7 +12,6 @@ import com.tangem.core.ui.components.HoldToConfirmButton import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.R as CoreR import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe @@ -55,10 +54,7 @@ internal fun WcTransactionRequestButtons( isHoldToConfirmEnabled -> { HoldToConfirmButton( modifier = buttonModifier, - text = stringResourceSafe( - CoreR.string.common_hold_to, - activeButtonText.resolveReference(), - ), + text = activeButtonText.resolveReference(), onConfirm = onClickActiveButton, isLoading = isLoading, enabled = enabled, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/YieldSupplyApproveComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/YieldSupplyApproveComponent.kt index f56edfc633..dc2ffd7777 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/YieldSupplyApproveComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/YieldSupplyApproveComponent.kt @@ -20,7 +20,6 @@ import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.HoldToConfirmButton import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.R as CoreR import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle @@ -82,10 +81,7 @@ internal class YieldSupplyApproveComponent( if (state.isHoldToConfirmEnabled) { HoldToConfirmButton( - text = stringResourceSafe( - CoreR.string.common_hold_to, - stringResourceSafe(R.string.common_confirm), - ), + text = stringResourceSafe(R.string.common_confirm), onConfirm = model::onClick, enabled = state.isPrimaryButtonEnabled, isLoading = state.isTransactionSending, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt index c7957a5d41..c561bab7ca 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt @@ -26,7 +26,6 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.core.ui.R as CoreR import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyActionUM import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyFeeUM @@ -114,10 +113,7 @@ internal class YieldSupplyStartEarningComponent( if (state.isHoldToConfirmEnabled) { HoldToConfirmButton( - text = stringResourceSafe( - CoreR.string.common_hold_to, - stringResourceSafe(R.string.yield_module_start_earning), - ), + text = stringResourceSafe(R.string.yield_module_start_earning), onConfirm = model::onClick, enabled = state.isPrimaryButtonEnabled, isLoading = state.isTransactionSending, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt index eae26ea8e1..58c0a5e000 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt @@ -21,7 +21,6 @@ import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.HoldToConfirmButton import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.R as CoreR import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle @@ -86,10 +85,7 @@ internal class YieldSupplyStopEarningComponent( if (state.isHoldToConfirmEnabled) { HoldToConfirmButton( - text = stringResourceSafe( - CoreR.string.common_hold_to, - stringResourceSafe(R.string.common_confirm), - ), + text = stringResourceSafe(R.string.common_confirm), onConfirm = model::onClick, enabled = state.isPrimaryButtonEnabled, isLoading = state.isTransactionSending, From 6252cc6a4b13c4acb4a7e0813eb4465a0fa26c54 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 12:47:55 +0000 Subject: [PATCH 054/106] 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 4372af7f4b..6d72aefb0d 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.33-1423" +tangemBlockchainSdk = "develop-1425" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.33-576" +tangemCardSdk = "develop-577" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-545" +tangemHotSdk = "develop-539" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From 20289a89fffcbf2956b42dacd88787adabce6d85 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 20:07:23 +0400 Subject: [PATCH 055/106] Updated on 2026-08-14 --- .../tangem/features/tangempay/model/TangemPayDetailsModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8dfb45c420..aa37f99d4a 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 @@ -437,7 +437,7 @@ internal class TangemPayDetailsModel @Inject constructor( analytics.send(TangemPayAnalyticsEvents.ReceiveFundsClicked()) bottomSheetNavigation.dismiss() val config = TokenReceiveConfig( - shouldShowWarning = false, + shouldShowWarning = true, cryptoCurrency = data.currency, userWalletId = data.walletId, showMemoDisclaimer = false, From 9275e7d178c4482eb91f11b5bd7fbd034c1e4b80 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 19:25:31 +0300 Subject: [PATCH 056/106] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 2 +- gradle/tangem_dependencies.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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 cdbd47714a..c04b7ea0fc 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 @@ -77,6 +77,6 @@ }, { "name": "WALLET_REORDER_FEATURE_ENABLED", - "version": "undefined" + "version": "5.34" } ] diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 6d72aefb0d..a5765e9809 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 = "develop-1425" +tangemBlockchainSdk = "releases-5.34-1428" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-577" +tangemCardSdk = "releases-5.34-579" #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 00974e5d3dd9ba51f675999b6a0a2ecf2ccf1982 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Feb 2026 09:28:22 +0100 Subject: [PATCH 057/106] Updated on 2026-08-14 --- .../core/ui/utils/DateTimeFormatters.kt | 19 ++- .../core/ui/utils/DateTimeFormattersTest.kt | 143 ++++++++++++++++++ .../feed/ui/utils/CreatedTimeFormatter.kt | 11 +- 3 files changed, 166 insertions(+), 7 deletions(-) create mode 100644 core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateTimeFormattersTest.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index 95ffc567c2..f460a6f129 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -103,9 +103,9 @@ object DateTimeFormatters { */ val localFullDate: DateTimeFormatter by lazy { val locale = Locale.getDefault() - val datePattern = DateFormat.getBestDateTimePattern(locale, "d MMMM") + val datePattern = icuPatternToJodaPattern(DateFormat.getBestDateTimePattern(locale, "d MMMM")) val timeSkeleton = if (is12HourFormat) "h:mm a" else "HH:mm" - val timePattern = DateFormat.getBestDateTimePattern(locale, timeSkeleton) + val timePattern = icuPatternToJodaPattern(DateFormat.getBestDateTimePattern(locale, timeSkeleton)) val fullPattern = "$datePattern, $timePattern" DateTimeFormatterBuilder() .appendPattern(fullPattern) @@ -128,13 +128,26 @@ object DateTimeFormatters { */ fun getBestFormatterBySkeleton(skeleton: String): DateTimeFormatter { val skeletonWithLocale = skeleton.replaceHourLetters() + val icuPattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeletonWithLocale) + val jodaPattern = icuPatternToJodaPattern(icuPattern) return DateTimeFormatterBuilder() - .appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), skeletonWithLocale)) + .appendPattern(jodaPattern) .toFormatter() .withLocale(Locale.getDefault()) } + /** + * Converts ICU date/time pattern (from [DateFormat.getBestDateTimePattern]) to Joda-Time compatible pattern. + */ + internal fun icuPatternToJodaPattern(icuPattern: String): String { + return icuPattern + .replace("LLLL", "MMMM") + .replace("LLL", "MMM") + .replace("LL", "MM") + .replace("L", "M") + } + private fun String.replaceHourLetters(): String { return if (is12HourFormat) { this.replace('H', 'h').replace('k', 'K') diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateTimeFormattersTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateTimeFormattersTest.kt new file mode 100644 index 0000000000..7fa8bcd9ce --- /dev/null +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateTimeFormattersTest.kt @@ -0,0 +1,143 @@ +package com.tangem.core.ui.utils + +import com.google.common.truth.Truth +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** + * Unit tests for [DateTimeFormatters], in particular for conversion of ICU date/time patterns + * to Joda-Time compatible patterns. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DateTimeFormattersTest { + + @Test + fun `converts LLLL to MMMM - full standalone month pattern that crashes on Chinese locale`() { + // Arrange + val icuPattern = "d LLLL" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("d MMMM") + } + + @Test + fun `converts LLL to MMM - short standalone month`() { + // Arrange + val icuPattern = "dd LLL yyyy" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("dd MMM yyyy") + } + + @Test + fun `converts LL to MM - numeric standalone month`() { + // Arrange + val icuPattern = "yyyy-MM-LL" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("yyyy-MM-MM") + } + + @Test + fun `converts single L to M`() { + // Arrange + val icuPattern = "d/L/yyyy" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("d/M/yyyy") + } + + @Test + fun `leaves pattern without L unchanged`() { + // Arrange + val icuPattern = "dd.MM.yyyy HH:mm" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("dd.MM.yyyy HH:mm") + } + + @Test + fun `leaves pattern with M unchanged`() { + // Arrange + val icuPattern = "d MMMM yyyy" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("d MMMM yyyy") + } + + @Test + fun `handles mixed ICU pattern as returned for Chinese locale - d MMMM`() { + // Arrange + val icuPatternWithStandaloneMonth = "d LLLL" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPatternWithStandaloneMonth) + + // Assert — Joda-Time can parse and format this without IllegalArgumentException + Truth.assertThat(actual).isEqualTo("d MMMM") + } + + @Test + fun `handles empty string`() { + // Arrange + val icuPattern = "" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEmpty() + } + + @Test + fun `handles pattern with only literal characters`() { + // Arrange + val icuPattern = " 'at' " + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo(" 'at' ") + } + + @Test + fun `getBestFormatterBySkeleton with d MMMM skeleton produces formatter that does not throw on format`() { + // Arrange + val formatter = DateTimeFormatters.getBestFormatterBySkeleton("d MMMM") + val date = org.joda.time.DateTime(2025, 2, 13, 12, 0, 0, 0) + + // Act & Assert + val formatted = formatter.print(date) + Truth.assertThat(formatted).isNotEmpty() + } + + @Test + fun `getBestFormatterBySkeleton with dd MMMM skeleton produces formatter that does not throw on format`() { + // Arrange + val formatter = DateTimeFormatters.getBestFormatterBySkeleton("dd MMMM") + val date = org.joda.time.DateTime(2025, 2, 13, 12, 0, 0, 0) + + // Act & Assert + val formatted = formatter.print(date) + Truth.assertThat(formatted).isNotEmpty() + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/CreatedTimeFormatter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/CreatedTimeFormatter.kt index b1052994f0..ef92d71b3c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/CreatedTimeFormatter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/CreatedTimeFormatter.kt @@ -10,10 +10,13 @@ import com.tangem.utils.StringsSigns import org.joda.time.DateTime internal fun mapFormattedDate(createdAt: String): TextReference { - val formattedDate = getFormattedDate( - createdAt = createdAt, - now = DateTime.now(), - ) + val formattedDate = runCatching { + getFormattedDate( + createdAt = createdAt, + now = DateTime.now(), + ) + }.getOrElse { FormattedDate.FullDate("") } + return when (formattedDate) { is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date) is FormattedDate.HoursAgo -> TextReference.PluralRes( From 2cc7fdc952b80ca97b42f2717811e44770ce72d7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Feb 2026 14:16:56 +0400 Subject: [PATCH 058/106] Updated on 2026-08-14 --- .../producer/WalletAccountListFlowFactory.kt | 3 +- .../WalletAccountListFlowFactoryTest.kt | 38 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt index 2fe0b92a3f..fbc60cc6c6 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt @@ -47,8 +47,9 @@ internal class WalletAccountListFlowFactory @Inject constructor( return accountsResponseStoreFactory.create(userWallet.walletId).data .filterNotNull() + .filter { it.accounts.isNotEmpty() } .distinctUntilChanged() - .map(converter::convert) + .map { converter.convert(it) } } private fun createForSingleWallet(userWallet: UserWallet): AccountList { diff --git a/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt index bcda235987..335516f9fd 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt @@ -169,4 +169,42 @@ class WalletAccountListFlowFactoryTest { accountListConverter.convert(any()) } } + + @Test + fun `create for multi wallet with empty accounts does not emit`() = runTest { + // Arrange + val userWallet = mockk { + every { this@mockk.walletId } returns userWalletId + every { this@mockk.isMultiCurrency } returns true + } + + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow + + val accountsResponseWithEmptyAccounts = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + group = null, + sort = null, + totalAccounts = 0, + totalArchivedAccounts = 0, + ), + accounts = emptyList(), + unassignedTokens = emptyList(), + ) + every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore + every { accountsResponseStore.data } returns accountsResponseStoreFlow + accountsResponseStoreFlow.value = accountsResponseWithEmptyAccounts + + // Act + val actual = factory.create(userWalletId).let(::getEmittedValues) + + // Assert + Truth.assertThat(actual).isEmpty() + + coVerify(inverse = true) { + accountListConverterFactory.create(any()) + accountListConverter.convert(any()) + } + } } \ No newline at end of file From 1d9d74b6a98377c1f28b7b83c0f5d39d6ddbed0a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Feb 2026 17:12:27 +0300 Subject: [PATCH 059/106] 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 a5d1a89425..2d18650f6d 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit a5d1a89425a95bc9c90c7a6fed3c578b0d324994 +Subproject commit 2d18650f6d4286353046ccb841745cef107c4fa6 From fe2bade1e49699ca493767754299a6e75319f22e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Feb 2026 14:07:02 +0300 Subject: [PATCH 060/106] Updated on 2026-08-14 --- features/onramp/impl/build.gradle.kts | 1 + .../swap/DefaultSwapSelectTokensComponent.kt | 31 ++++++ .../AvailableSwapPairsComponent.kt | 8 ++ .../DefaultAvailableSwapPairsComponent.kt | 8 ++ .../market/SwapMarketsListBatchFlowManager.kt | 7 ++ .../model/AddToPortfolioRoute.kt | 7 ++ .../model/AvailableSwapPairsModel.kt | 103 +++++++++++++++++- 7 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AddToPortfolioRoute.kt diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index d836fce188..f0a6cb16c5 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.features.swap.domain) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) + implementation(projects.features.feed.api) /** Project - Core */ implementation(projects.core.analytics) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt index d9b66c559d..ee2dc2dd5b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt @@ -5,13 +5,20 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent import com.tangem.features.onramp.component.SwapSelectTokensComponent import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent +import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute import com.tangem.features.onramp.swap.model.SwapSelectTokensModel import com.tangem.features.onramp.swap.ui.SwapSelectTokens import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent @@ -25,6 +32,7 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor( tokenListComponentFactory: OnrampTokenListComponent.Factory, availableSwapPairsComponentFactory: AvailableSwapPairsComponent.Factory, analyticsEventHandler: AnalyticsEventHandler, + private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, @Assisted private val appComponentContext: AppComponentContext, @Assisted private val params: SwapSelectTokensComponent.Params, ) : AppComponentContext by appComponentContext, SwapSelectTokensComponent { @@ -49,15 +57,36 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor( ), ) + private val bottomSheetSlot = childSlot( + source = selectToTokenListComponent.bottomSheetNavigation, + serializer = AddToPortfolioRoute.serializer(), + key = "add_to_portfolio_bottom_sheet", + handleBackButton = false, + childFactory = { _, context -> bottomSheetChild(context) }, + ) + init { analyticsEventHandler.send(event = MainScreenAnalyticsEvent.SwapScreenOpened()) } + @Suppress("UnsafeCallOnNullableType") + private fun bottomSheetChild(componentContext: ComponentContext): ComposableBottomSheetComponent { + return addToPortfolioComponentFactory.create( + context = childByContext(componentContext), + params = AddToPortfolioComponent.Params( + addToPortfolioManager = selectToTokenListComponent.addToPortfolioManager!!, + callback = selectToTokenListComponent.addToPortfolioCallback, + shouldSkipTokenActionsScreen = true, + ), + ) + } + @Composable override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() val fromTokensState by selectFromTokenListComponent.uiState.collectAsStateWithLifecycle() val toTokensState by selectToTokenListComponent.uiState.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() SwapSelectTokens( state = state, @@ -67,6 +96,8 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor( selectToTokenListState = toTokensState, modifier = modifier, ) + + bottomSheet.child?.instance?.BottomSheet() } @AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt index cb87a50dc2..bae0b5ba51 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt @@ -1,11 +1,15 @@ package com.tangem.features.onramp.swap.availablepairs import androidx.compose.runtime.Stable +import com.arkivanov.decompose.router.slot.SlotNavigation import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.decompose.ComposableListContentComponent import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId +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.onramp.swap.availablepairs.model.AddToPortfolioRoute import com.tangem.features.onramp.tokenlist.entity.TokenListUM import kotlinx.coroutines.flow.StateFlow @@ -13,6 +17,10 @@ import kotlinx.coroutines.flow.StateFlow @Stable internal interface AvailableSwapPairsComponent : ComposableListContentComponent { + val bottomSheetNavigation: SlotNavigation + val addToPortfolioManager: AddToPortfolioManager? + val addToPortfolioCallback: AddToPortfolioComponent.Callback + /** Component factory */ interface Factory : ComponentFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt index 456b6e8c30..27e528cd04 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt @@ -3,8 +3,12 @@ package com.tangem.features.onramp.swap.availablepairs import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier +import com.arkivanov.decompose.router.slot.SlotNavigation import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +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.onramp.swap.availablepairs.model.AddToPortfolioRoute import com.tangem.features.onramp.swap.availablepairs.model.AvailableSwapPairsModel import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.ui.onrampSwapTokenList @@ -21,6 +25,10 @@ internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor( private val model: AvailableSwapPairsModel = getOrCreateModel(params) + override val bottomSheetNavigation: SlotNavigation get() = model.bottomSheetNavigation + override val addToPortfolioManager: AddToPortfolioManager? get() = model.addToPortfolioManager + override val addToPortfolioCallback: AddToPortfolioComponent.Callback get() = model.addToPortfolioCallback + override val uiState: StateFlow get() = model.state diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt index 2aecafa4e4..8d9b34c5b8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt @@ -228,6 +228,13 @@ internal class SwapMarketsListBatchFlowManager( } } + fun getTokenMarketById(id: CryptoCurrency.RawID): TokenMarket? { + return batchFlow.state.value.data + .asSequence() + .flatMap { it.data } + .firstOrNull { it.id == id } + } + fun getBatchKeysByItemIds(ids: List): Set { val currentData = batchFlow.state.value.data diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AddToPortfolioRoute.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AddToPortfolioRoute.kt new file mode 100644 index 0000000000..559ef6eb09 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AddToPortfolioRoute.kt @@ -0,0 +1,7 @@ +package com.tangem.features.onramp.swap.availablepairs.model + +import com.tangem.core.decompose.navigation.Route +import kotlinx.serialization.Serializable + +@Serializable +internal data object AddToPortfolioRoute : Route \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index 5466084880..7867772800 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -9,28 +9,36 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase 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.core.lce.Lce import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.toSerializableParam 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.tokenlist.TokenList +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.swap.domain.GetAvailablePairsUseCase import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.feature.swap.domain.models.domain.SwapPairLeast +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.onramp.impl.R +import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent import com.tangem.features.swap.SwapFeatureToggles import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer @@ -48,9 +56,17 @@ import com.tangem.features.onramp.tokenlist.entity.transformer.* import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.lib.crypto.BlockchainUtils +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.utils.Provider 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 kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -71,7 +87,10 @@ internal class AvailableSwapPairsModel @Inject constructor( private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val accountsFeatureToggles: AccountsFeatureToggles, private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, - private val swapFeatureToggles: SwapFeatureToggles, + private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, + private val excludedBlockchains: ExcludedBlockchains, + private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, + swapFeatureToggles: SwapFeatureToggles, getWalletsUseCase: GetWalletsUseCase, ) : Model() { @@ -79,6 +98,17 @@ internal class AvailableSwapPairsModel @Inject constructor( private val params: AvailableSwapPairsComponent.Params = paramsContainer.require() private val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } + private val allUserWallets = getWalletsUseCase.invokeSync() + + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + var addToPortfolioManager: AddToPortfolioManager? = null + val addToPortfolioCallback: AddToPortfolioComponent.Callback = object : AddToPortfolioComponent.Callback { + override fun onDismiss() = bottomSheetNavigation.dismiss() + override fun onSuccess(addedToken: CryptoCurrency) { + onTokenAddedToPortfolio(addedToken) + } + } + private val addToPortfolioJobHolder = JobHolder() private val tokenListFlow = getTokenListUseCaseFlow() private val accountListFlow = getAccountListUseCaseFlow() @@ -86,6 +116,7 @@ internal class AvailableSwapPairsModel @Inject constructor( private val selectedAppCurrencyFlow: StateFlow = getSelectedAppCurrencyUseCase.invokeOrDefault() .stateIn(scope = modelScope, started = SharingStarted.Eagerly, initialValue = AppCurrency.Default) + private val refreshPairsTrigger = MutableSharedFlow() private val searchQueryStateForMarkets = MutableStateFlow("") private val visibleMarketItemIds = MutableStateFlow>(emptyList()) @@ -374,8 +405,12 @@ internal class AvailableSwapPairsModel @Inject constructor( private fun subscribeOnAvailablePairsUpdates() { modelScope.launch { - params.selectedStatus - .filterNotNull() + combine( + params.selectedStatus.filterNotNull(), + refreshPairsTrigger + .onEach { availablePairsByNetworkFlow.value = emptyMap() } + .onStart { emit(Unit) }, + ) { status, _ -> status } .collectLatest { selectedStatus -> val networkInfo = selectedStatus.toLeastTokenInfo() @@ -542,7 +577,7 @@ internal class AvailableSwapPairsModel @Inject constructor( else -> SwapMarketState.Content( items = uiItems, loadMore = { searchMarketsListManager.loadMore() }, - onItemClick = {}, + onItemClick = { item -> addToPortfolioItem(item) }, visibleIdsChanged = { visibleMarketItemIds.value = it }, total = total ?: uiItems.size, ) @@ -556,6 +591,66 @@ internal class AvailableSwapPairsModel @Inject constructor( .launchIn(modelScope) } + private fun onTokenAddedToPortfolio(addedToken: CryptoCurrency) { + modelScope.launch { + bottomSheetNavigation.dismiss() + + // Trigger re-fetch of available pairs (clears cache + re-enters collectLatest) + refreshPairsTrigger.emit(Unit) + + // Wait for the added token status to become Loaded + val addedTokenStatus = getAccountCurrencyStatusUseCase(params.userWalletId, addedToken) + .firstOrNull { it.status.value is CryptoCurrencyStatus.Loaded } + ?.status + ?: return@launch + + // Convert to TokenItemState and trigger token selection → navigates to swap + val converter = OnrampTokenItemStateConverterFactory.createAvailableItemConverter( + appCurrency = selectedAppCurrencyFlow.value, + onItemClick = params.onTokenClick, + ) + params.onTokenClick(converter.convert(addedTokenStatus), addedTokenStatus) + } + } + + private fun addToPortfolioItem(item: MarketsListItemUM) { + modelScope.launch { + val tokenMarket = searchMarketsListManager.getTokenMarketById(item.id) ?: return@launch + + val param = tokenMarket.toSerializableParam() + val hasOnlyHotWallets = allUserWallets.all { it is UserWallet.Hot } + + val networks = tokenMarket.networks?.filter { network -> + BlockchainUtils.isSupportedNetworkId( + blockchainId = network.networkId, + 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 = null, + ).apply { + setTokenNetworks(networks) + } + + addToPortfolioManager?.state + ?.firstOrNull { it is AddToPortfolioManager.State.AvailableToAdd } + ?.run { bottomSheetNavigation.activate(AddToPortfolioRoute) } + }.saveIn(addToPortfolioJobHolder) + } + private fun subscribeOnVisibleMarketItems() { modelScope.launch { visibleMarketItemIds.mapNotNull { rawIds -> From 326d74753614e68505a9e06de26a5d2906d04a76 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Feb 2026 15:48:18 +0300 Subject: [PATCH 061/106] Updated on 2026-08-14 --- .../market/SwapMarketsListBatchFlowManager.kt | 3 +- .../market/state/SwapMarketState.kt | 19 +- .../model/AvailableSwapPairsAnalyticsEvent.kt | 44 +++ .../model/AvailableSwapPairsModel.kt | 171 +++++++++-- .../availablepairs/ui/SwapMarketsListItems.kt | 3 +- .../onramp/tokenlist/ui/OnrampTokenList.kt | 16 +- .../feature/swap/analytics/SwapEvents.kt | 67 ++++- .../tangem/feature/swap/model/SwapModel.kt | 265 ++++++++++++++---- .../swap/model/SwapNotificationsFactory.kt | 6 + .../market/SwapMarketsListBatchFlowManager.kt | 6 +- .../models/market/state/SwapMarketState.kt | 19 +- .../swap/models/states/SwapNotificationUM.kt | 12 + .../tangem/feature/swap/ui/StateBuilder.kt | 55 ++++ .../feature/swap/ui/SwapSelectTokenScreen.kt | 22 +- .../ui/market/SwapMarketsListLazyColumn.kt | 3 +- .../preview/SwapSelectTokenPreviewProvider.kt | 4 + 16 files changed, 597 insertions(+), 118 deletions(-) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt index 8d9b34c5b8..5df10fda71 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.* internal class SwapMarketsListBatchFlowManager( getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType, + private val order: TokenMarketListConfig.Order, private val currentAppCurrency: Provider, private val currentSearchText: Provider, private val modelScope: CoroutineScope, @@ -183,7 +184,7 @@ internal class SwapMarketsListBatchFlowManager( searchText ?: currentSearchText() }, priceChangeInterval = TokenMarketListConfig.Interval.H24, - order = TokenMarketListConfig.Order.ByRating, + order = order, shouldNetworks = true, ), ), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt index bcd1c46b86..7e9abb3982 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt @@ -2,25 +2,40 @@ package com.tangem.features.onramp.swap.availablepairs.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 kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class SwapMarketState { + abstract val marketsTitle: TextReference + abstract val shouldAssetsCount: Boolean + data class Content( val items: ImmutableList, val total: Int, val loadMore: () -> Unit, val onItemClick: (MarketsListItemUM) -> Unit, val visibleIdsChanged: (List) -> Unit, + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, ) : SwapMarketState() - data object Loading : SwapMarketState() + data class Loading( + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, + ) : SwapMarketState() data class LoadingError( val onRetryClicked: () -> Unit, + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, ) : SwapMarketState() - data object SearchNothingFound : SwapMarketState() + data object SearchNothingFound : SwapMarketState() { + override val marketsTitle: TextReference = TextReference.Res(R.string.markets_common_title) + override val shouldAssetsCount: Boolean = true + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt new file mode 100644 index 0000000000..f5a57df2b5 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt @@ -0,0 +1,44 @@ +package com.tangem.features.onramp.swap.availablepairs.model + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM + +private const val SWAP_CATEGORY = "Swap" + +internal sealed class AvailableSwapPairsAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(SWAP_CATEGORY, event, params) { + + class TokenSelected( + val token: String, + val source: String, + val isSearched: Boolean, + ) : AvailableSwapPairsAnalyticsEvent( + event = "Token Selected", + params = mapOf( + TOKEN_PARAM to token, + SOURCE to source, + SEARCHED to if (isSearched) "True" else "False", + ), + ) { + companion object { + const val SOURCE = "Source" + const val SEARCHED = "Searched" + const val SOURCE_PORTFOLIO = "Portfolio" + const val SOURCE_MARKETS = "Markets" + } + } + + class TokenAdded( + val token: String, + val blockchain: String, + ) : AvailableSwapPairsAnalyticsEvent( + event = "Token Added", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), + ) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index 7867772800..62822d4108 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -1,8 +1,12 @@ package com.tangem.features.onramp.swap.availablepairs.model +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.fields.InputManager +import com.tangem.core.ui.R as CoreUiR +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList @@ -22,6 +26,7 @@ import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading 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.account.AccountStatus @@ -66,6 +71,7 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.runSuspendCatching +import kotlinx.coroutines.ExperimentalCoroutinesApi import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -77,6 +83,7 @@ private typealias AvailablePairsState = Lce> internal class AvailableSwapPairsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, private val getTokenListUseCase: GetTokenListUseCase, private val tokenListUMController: TokenListUMController, private val searchManager: InputManager, @@ -120,10 +127,23 @@ internal class AvailableSwapPairsModel @Inject constructor( private val searchQueryStateForMarkets = MutableStateFlow("") private val visibleMarketItemIds = MutableStateFlow>(emptyList()) + 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 { searchQueryStateForMarkets.value }, modelScope = modelScope, @@ -131,6 +151,8 @@ internal class AvailableSwapPairsModel @Inject constructor( ) } + private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) + init { if (accountsFeatureToggles.isFeatureEnabled) { subscribeOnUpdateStateV2() @@ -298,7 +320,7 @@ internal class AvailableSwapPairsModel @Inject constructor( } else { UpdateTokenItemsTransformer( appCurrency = appCurrency, - onItemClick = params.onTokenClick, + onItemClick = ::onPortfolioTokenClick, statuses = filterByQueryTokenList.filterByAvailability(availablePairs = availablePairs), isBalanceHidden = isBalanceHidden, unavailableTokensHeaderReference = resourceReference( @@ -357,7 +379,7 @@ internal class AvailableSwapPairsModel @Inject constructor( } else { UpdateAccountTokenListTransformer( appCurrency = appCurrency, - onItemClick = params.onTokenClick, + onItemClick = ::onPortfolioTokenClick, accountList = filterByQueryAccountList.filterByAvailability(availablePairs = availablePairs), isBalanceHidden = isBalanceHidden, unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), @@ -543,6 +565,17 @@ internal class AvailableSwapPairsModel @Inject constructor( } } + private fun onPortfolioTokenClick(tokenItem: TokenItemState, status: CryptoCurrencyStatus) { + analyticsEventHandler.send( + AvailableSwapPairsAnalyticsEvent.TokenSelected( + token = status.currency.symbol, + source = AvailableSwapPairsAnalyticsEvent.TokenSelected.SOURCE_PORTFOLIO, + isSearched = state.value.searchBarUM.query.isNotEmpty(), + ), + ) + params.onTokenClick(tokenItem, status) + } + private fun CryptoCurrencyStatus.toLeastTokenInfo(): LeastTokenInfo { return LeastTokenInfo( contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", @@ -550,50 +583,121 @@ internal class AvailableSwapPairsModel @Inject constructor( ) } - @Suppress("LongMethod") + @OptIn(ExperimentalCoroutinesApi::class) private fun subscribeOnMarketsUpdates() { - combine( - flow = searchQueryStateForMarkets - .onEach { searchQuery -> - if (searchQuery.isNotEmpty()) { - searchMarketsListManager.reload(searchQuery) - } - }, - flow2 = searchMarketsListManager.uiItems, - flow3 = searchMarketsListManager.isInInitialLoadingErrorState, - flow4 = searchMarketsListManager.isSearchNotFoundState, - flow5 = searchMarketsListManager.totalCount, - ) { searchQuery, uiItems, isError, isSearchNotFound, total -> - when { - searchQuery.isEmpty() -> { + searchQueryStateForMarkets + .map { it.isEmpty() } + .distinctUntilChanged() + .flatMapLatest { isDefaultMode -> + if (isDefaultMode) { visibleMarketItemIds.value = emptyList() - null + createDefaultMarketsFlow() + } else { + visibleDefaultMarketItemIds.value = emptyList() + createSearchMarketsFlow() } + } + .onEach { marketsState -> + tokenListUMController.update { it.copy(marketsState = marketsState) } + } + .flowOn(dispatchers.main) + .launchIn(modelScope) + + searchQueryStateForMarkets + .onEach { searchQuery -> + if (searchQuery.isNotEmpty()) { + searchMarketsListManager.reload(searchQuery) + } + } + .launchIn(modelScope) + + params.selectedStatus + .filterNotNull() + .take(1) + .onEach { defaultMarketsListManager.reload() } + .launchIn(modelScope) + } + + private fun createDefaultMarketsFlow(): Flow { + val marketsTitle = TextReference.Res(CoreUiR.string.feed_trending_now) + return combine( + defaultMarketsListManager.uiItems, + defaultMarketsListManager.isInInitialLoadingErrorState, + defaultMarketsListManager.totalCount, + ) { uiItems, isError, total -> + when { isError -> SwapMarketState.LoadingError( - onRetryClicked = { searchMarketsListManager.reload(searchQuery) }, + 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(CoreUiR.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(searchQueryStateForMarkets.value) + }, + marketsTitle = marketsTitle, + shouldAssetsCount = true, ) isSearchNotFound -> SwapMarketState.SearchNothingFound - uiItems.isEmpty() -> SwapMarketState.Loading + 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, ) } } - .distinctUntilChanged() - .onEach { marketsState -> - tokenListUMController.update { it.copy(marketsState = marketsState) } - } - .flowOn(dispatchers.main) - .launchIn(modelScope) } private fun onTokenAddedToPortfolio(addedToken: CryptoCurrency) { modelScope.launch { bottomSheetNavigation.dismiss() + analyticsEventHandler.send( + AvailableSwapPairsAnalyticsEvent.TokenAdded( + token = addedToken.symbol, + blockchain = addedToken.network.name, + ), + ) + analyticsEventHandler.send( + AvailableSwapPairsAnalyticsEvent.TokenSelected( + token = addedToken.symbol, + source = AvailableSwapPairsAnalyticsEvent.TokenSelected.SOURCE_MARKETS, + isSearched = state.value.searchBarUM.query.isNotEmpty(), + ), + ) // Trigger re-fetch of available pairs (clears cache + re-enters collectLatest) refreshPairsTrigger.emit(Unit) @@ -615,7 +719,9 @@ internal class AvailableSwapPairsModel @Inject constructor( private fun addToPortfolioItem(item: MarketsListItemUM) { modelScope.launch { - val tokenMarket = searchMarketsListManager.getTokenMarketById(item.id) ?: return@launch + val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id) + ?: searchMarketsListManager.getTokenMarketById(item.id) + ?: return@launch val param = tokenMarket.toSerializableParam() val hasOnlyHotWallets = allUserWallets.all { it is UserWallet.Hot } @@ -663,5 +769,16 @@ internal class AvailableSwapPairsModel @Inject constructor( searchMarketsListManager.loadCharts(visibleBatchKeys) } } + modelScope.launch { + visibleDefaultMarketItemIds.mapNotNull { rawIds -> + if (rawIds.isNotEmpty()) { + defaultMarketsListManager.getBatchKeysByItemIds(rawIds) + } else { + null + } + }.distinctUntilChanged().collectLatest { visibleBatchKeys -> + defaultMarketsListManager.loadCharts(visibleBatchKeys) + } + } } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt index b8e098fa31..18600da888 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt @@ -17,6 +17,7 @@ import com.tangem.common.ui.markets.MarketsListItemPlaceholder import com.tangem.core.ui.R import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState @@ -28,7 +29,7 @@ internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) { val totalCount = (state as? SwapMarketState.Content)?.total Text( text = buildAnnotatedString { - append(stringResourceSafe(R.string.markets_common_title)) + append(state.marketsTitle.resolveReference()) if (totalCount != null) { withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { append(" $totalCount") diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index 6cecf0ac0e..d92870b44f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -50,8 +50,7 @@ import kotlinx.collections.immutable.ImmutableList * */ internal fun LazyListScope.onrampSwapTokenList(state: TokenListUM) { - val isSearchMode = state.searchBarUM.query.isNotEmpty() && state.marketsState != null - if (isSearchMode) { + if (state.marketsState != null) { onrampTokenListWithMarkets(state = state) } else { onrampTokenList(state = state) @@ -91,7 +90,10 @@ private fun LazyListScope.onrampTokenListWithMarkets(state: TokenListUM) { state.tokensListData.totalTokensCount != 0 if (hasAssets) { - assetsTitle(count = state.tokensListData.totalTokensCount) + assetsTitle( + count = state.tokensListData.totalTokensCount, + showCount = state.marketsState?.shouldAssetsCount == true, + ) tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) @@ -159,13 +161,15 @@ private fun LazyListScope.searchBarItem(searchBarUM: SearchBarUM, modifier: Modi } } -private fun LazyListScope.assetsTitle(count: Int) { +private fun LazyListScope.assetsTitle(count: Int, showCount: Boolean) { item(key = "assets_title") { Text( text = buildAnnotatedString { append(stringResourceSafe(R.string.swap_your_assets_title)) - withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { - append(" $count") + if (showCount) { + withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { + append(" $count") + } } }, style = TangemTheme.typography.h3, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 98955f6373..cd44e2f95c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -25,7 +25,7 @@ sealed class SwapEvents( params: Map = emptyMap(), ) : AnalyticsEvent(SWAP_CATEGORY, event, params) { - data class SwapScreenOpened( + class SwapScreenOpened( val token: String, val blockchain: String, ) : SwapEvents( @@ -38,25 +38,47 @@ sealed class SwapEvents( class SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") - data class ChooseTokenScreenOpened(val hasAvailableTokens: Boolean) : SwapEvents( + class ChooseTokenScreenOpened(val hasAvailableTokens: Boolean) : SwapEvents( event = "Choose Token Screen Opened", params = mapOf("Available tokens" to if (hasAvailableTokens) "Yes" else "No"), ) - data class ChooseTokenScreenResult(val isTokenChosen: Boolean, val token: String? = null) : SwapEvents( + class ChooseTokenScreenResult( + val isTokenChosen: Boolean, + val token: String? = null, + val source: String? = null, + val isSearched: Boolean? = null, + ) : SwapEvents( event = "Choose Token Screen Result", params = buildMap { put("Token Chosen", if (isTokenChosen) "Yes" else "No") token?.let { put("Token", it) } + source?.let { put(TOKEN_SELECTED_SOURCE, it) } + isSearched?.let { put(SEARCHED, if (it) "True" else "False") } }, + ) { + companion object { + const val TOKEN_SELECTED_SOURCE = "Token Selected Source" + const val SEARCHED = "Searched" + const val SOURCE_PORTFOLIO = "Portfolio" + const val SOURCE_MARKETS = "Markets" + } + } + + class TokenAdded(val token: String, val blockchain: String) : SwapEvents( + event = "Token Added", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), ) - data class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents( + class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents( event = "Button - Swap", params = mapOf("Send Token" to sendToken, "Receive Token" to receiveToken), ) - data class ButtonGivePermissionClicked( + class ButtonGivePermissionClicked( val sendToken: String, val receiveToken: String, val provider: SwapProvider, @@ -69,7 +91,7 @@ sealed class SwapEvents( ), ) - data class ButtonPermissionApproveClicked( + class ButtonPermissionApproveClicked( val sendToken: String, val receiveToken: String, val approveType: ApproveType, @@ -88,8 +110,8 @@ sealed class SwapEvents( class ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") - @Suppress("NullableToStringCall") - data class SwapInProgressScreen( + @Suppress("NullableToStringCall", "LongParameterList") + class SwapInProgressScreen( val provider: SwapProvider, val commission: FeeType, // Market / Fast val sendBlockchain: String, @@ -118,24 +140,39 @@ sealed class SwapEvents( class ProviderClicked : SwapEvents("Provider Clicked") - data class ProviderChosen(val provider: SwapProvider) : SwapEvents( + class ProviderChosen(val provider: SwapProvider) : SwapEvents( event = "Provider Chosen", params = mapOf("Provider" to provider.name), ) - data class ButtonStatus(val token: String) : SwapEvents( + class ButtonStatus(val token: String) : SwapEvents( event = "Button - Status", params = mapOf("Token" to token), ) - data class ButtonExplore(val token: String) : SwapEvents( + class ButtonExplore(val token: String) : SwapEvents( event = "Button - Explore", params = mapOf("Token" to token), ) class NoticeNoAvailableTokensToSwap : SwapEvents("Notice - No Available Tokens To Swap") - data class NoticeNotEnoughFee(val token: String, val blockchain: String) : SwapEvents( + class NoticeUnavailableToSwapPair( + val sendToken: String, + val receiveToken: String, + val sendBlockchain: String, + val receiveBlockchain: String, + ) : SwapEvents( + event = "Notice - Unavailable To Swap Pair", + params = mapOf( + SEND_TOKEN to sendToken, + RECEIVE_TOKEN to receiveToken, + "Send Blockchain" to sendBlockchain, + "Receive Blockchain" to receiveBlockchain, + ), + ) + + class NoticeNotEnoughFee(val token: String, val blockchain: String) : SwapEvents( event = "Notice - Not Enough Fee", params = mapOf( "Token" to token, @@ -143,7 +180,7 @@ sealed class SwapEvents( ), ) - data class NoticeProviderError( + class NoticeProviderError( val sendToken: String, val receiveToken: String, val provider: SwapProvider, @@ -162,7 +199,7 @@ sealed class SwapEvents( // TODO parameters // region Promo activity - data class ChangellyActivity( + class ChangellyActivity( val promoState: PromoState, ) : AnalyticsEvent( category = PROMO_CATEGORY, @@ -177,7 +214,7 @@ sealed class SwapEvents( } } - data class NoticePermissionNeeded( + class NoticePermissionNeeded( val sendToken: String, val receiveToken: String, val provider: SwapProvider, 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 35c5fd4fa3..a90bd3640a 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 @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue + import arrow.core.Either import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation @@ -44,6 +45,7 @@ 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 @@ -88,7 +90,7 @@ 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.presentation.R +import com.tangem.core.ui.R import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder @@ -105,6 +107,7 @@ 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 kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* @@ -120,6 +123,7 @@ import javax.inject.Inject typealias SuccessLoadedSwapData = Map +@OptIn(ExperimentalCoroutinesApi::class) @Suppress("LongParameterList", "LargeClass") @Stable @ModelScoped @@ -154,7 +158,7 @@ internal class SwapModel @Inject constructor( private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, private val sendFeatureToggles: SendFeatureToggles, private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, - swapFeatureToggles: SwapFeatureToggles, + private val swapFeatureToggles: SwapFeatureToggles, private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, private val excludedBlockchains: ExcludedBlockchains, private val getUserWalletsUseCase: GetWalletsUseCase, @@ -258,10 +262,26 @@ internal class SwapModel @Inject constructor( 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, @@ -279,13 +299,21 @@ internal class SwapModel @Inject constructor( override fun onSuccess(addedToken: CryptoCurrency) { modelScope.launch { bottomSheetNavigation.dismiss() - uiState.selectTokenState?.let { currentSelectState -> - uiState = uiState.copy( - selectTokenState = currentSelectState.copy( - marketsState = null, - ), - ) - } + analyticsEventHandler.send( + SwapEvents.ChooseTokenScreenResult( + isTokenChosen = true, + token = addedToken.symbol, + source = SwapEvents.ChooseTokenScreenResult.SOURCE_MARKETS, + isSearched = searchQueryState.value.isNotEmpty(), + ), + ) + analyticsEventHandler.send( + SwapEvents.TokenAdded( + token = addedToken.symbol, + blockchain = addedToken.network.name, + ), + ) + searchQueryState.value = "" getAccountCurrencyStatusUseCase.invoke(userWalletId, addedToken) .firstOrNull { it.status.value is CryptoCurrencyStatus.Loaded @@ -362,50 +390,7 @@ internal class SwapModel @Inject constructor( } .launchIn(modelScope) - if (swapFeatureToggles.isMarketListFeatureEnabled) { - combine( - flow = searchQueryState - .onEach { searchQuery -> - searchMarketsListManager.reload(searchQuery) - }, - flow2 = searchMarketsListManager.uiItems, - flow3 = searchMarketsListManager.isInInitialLoadingErrorState, - flow4 = searchMarketsListManager.isSearchNotFoundState, - flow5 = searchMarketsListManager.totalCount.filterNotNull(), - ) { searchQuery, uiItems, isError, isSearchNotFound, total -> - when { - searchQuery.isEmpty() -> { - visibleMarketItemIds.value = emptyList() - null - } - isError -> SwapMarketState.LoadingError( - onRetryClicked = { searchMarketsListManager.reload(searchQuery) }, - ) - isSearchNotFound -> SwapMarketState.SearchNothingFound - uiItems.isEmpty() -> SwapMarketState.Loading - else -> SwapMarketState.Content( - items = uiItems, - loadMore = { searchMarketsListManager.loadMore() }, - onItemClick = { item -> - addToPortfolioItem(item) - }, - visibleIdsChanged = { visibleMarketItemIds.value = it }, - total = total, - ) - } - } - .distinctUntilChanged() - .onEach { marketsState -> - uiState.selectTokenState?.let { currentSelectState -> - uiState = uiState.copy( - selectTokenState = currentSelectState.copy( - marketsState = marketsState, - ), - ) - } - } - .launchIn(modelScope) - } + subscribeMarketTokens() modelScope.launch { visibleMarketItemIds.mapNotNull { rawIDS -> @@ -418,6 +403,18 @@ internal class SwapModel @Inject constructor( searchMarketsListManager.loadCharts(visibleBatchKeys) } } + + modelScope.launch { + visibleDefaultMarketItemIds.mapNotNull { rawIds -> + if (rawIds.isNotEmpty()) { + defaultMarketsListManager.getBatchKeysByItemIds(rawIds) + } else { + null + } + }.distinctUntilChanged().collectLatest { visibleBatchKeys -> + defaultMarketsListManager.loadCharts(visibleBatchKeys) + } + } } fun onStart() { @@ -443,6 +440,41 @@ 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) { @@ -613,6 +645,25 @@ internal class SwapModel @Inject constructor( toAccount = toAccount, tokensDataState = state, ) + + if (!isTokenAvailableForSwap(state, selectedCurrency, isReverseFromTo)) { + analyticsEventHandler.send( + SwapEvents.NoticeUnavailableToSwapPair( + sendToken = fromCurrencyStatus.currency.symbol, + receiveToken = toCurrencyStatus.currency.symbol, + sendBlockchain = fromCurrencyStatus.currency.network.name, + receiveBlockchain = toCurrencyStatus.currency.network.name, + ), + ) + uiState = stateBuilder.createSwapNotSupportedState( + uiStateHolder = uiState, + fromToken = fromCurrencyStatus, + toToken = toCurrencyStatus, + toAccount = toAccount, + ) + return + } + startLoadingQuotes( fromToken = fromCurrencyStatus, fromAccount = fromAccount, @@ -640,6 +691,17 @@ internal class SwapModel @Inject constructor( fromToken = dataState.fromCryptoCurrency?.currency ?: initialCurrencyFrom, ) } + latestMarketsState?.let(::applyMarketsState) + } + + private fun applyMarketsState(marketsState: SwapMarketState) { + uiState.selectTokenState?.let { currentSelectState -> + uiState = uiState.copy( + selectTokenState = currentSelectState.copy( + marketsState = marketsState, + ), + ) + } } private fun startLoadingQuotes( @@ -1275,7 +1337,14 @@ internal class SwapModel @Inject constructor( val (foundToken, foundAccount) = getSelectedTokenAndAccount(tokens, id) foundToken?.currency?.symbol?.let { symbol -> - analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = symbol)) + analyticsEventHandler.send( + SwapEvents.ChooseTokenScreenResult( + isTokenChosen = true, + token = symbol, + source = SwapEvents.ChooseTokenScreenResult.SOURCE_PORTFOLIO, + isSearched = searchQueryState.value.isNotEmpty(), + ), + ) } if (foundToken != null) { @@ -1871,6 +1940,28 @@ internal class SwapModel @Inject constructor( .orEmpty() } + private fun isTokenAvailableForSwap( + state: TokensDataStateExpress, + selectedCurrency: CryptoCurrencyStatus, + isReverseFromTo: Boolean, + ): Boolean { + val group = if (isReverseFromTo) state.fromGroup else state.toGroup + val idToFind = selectedCurrency.currency.id.value + + return if (accountsFeatureToggles.isFeatureEnabled) { + group.accountCurrencyList.any { (_, currencyList) -> + currencyList.any { accountSwapCurrency -> + idToFind == accountSwapCurrency.cryptoCurrencyStatus.currency.id.value && + accountSwapCurrency.isAvailable + } + } + } else { + group.available.any { swapAvailability -> + idToFind == swapAvailability.currencyStatus.currency.id.value + } + } + } + private fun List.filterForTangemPayWithdrawal(): List { return if (tangemPayInput?.isWithdrawal == true) { filter { it.type == ExchangeProviderType.CEX } @@ -2058,9 +2149,73 @@ 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 = searchMarketsListManager.getTokenMarketById(item.id) ?: return@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 } 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 6c9bd0a484..fdb774f17d 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 @@ -64,6 +64,12 @@ internal class SwapNotificationsFactory( ) } + fun getSwapNotSupportedNotifications(tokenName: String): ImmutableList { + return persistentListOf( + SwapNotificationUM.Warning.SwapNotSupported(tokenName), + ) + } + fun getQuotesErrorStateNotifications( expressDataError: ExpressDataError, fromToken: CryptoCurrency, 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/SwapMarketsListBatchFlowManager.kt index e8331efed3..77609fade7 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/SwapMarketsListBatchFlowManager.kt @@ -22,12 +22,14 @@ import kotlinx.coroutines.flow.* internal class SwapMarketsListBatchFlowManager( getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType, + private val order: TokenMarketListConfig.Order, private val currentAppCurrency: Provider, private val currentSearchText: Provider, private val modelScope: CoroutineScope, private val dispatchers: CoroutineDispatcherProvider, ) { - private val actionsFlow = MutableSharedFlow>() + private val actionsFlow = + MutableSharedFlow>(replay = 1) private val updateStateJob = JobHolder() private val batchFlow = getMarketsTokenListFlowUseCase( @@ -183,7 +185,7 @@ internal class SwapMarketsListBatchFlowManager( searchText ?: currentSearchText() }, priceChangeInterval = TokenMarketListConfig.Interval.H24, - order = TokenMarketListConfig.Order.ByRating, + order = order, shouldNetworks = true, ), ), 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 50e6d14a74..8be842bcfd 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,25 +2,40 @@ 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.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.core.ui.R import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class SwapMarketState { + abstract val marketsTitle: TextReference + abstract val shouldAssetsCount: Boolean + data class Content( val items: ImmutableList, val total: Int, val loadMore: () -> Unit, val onItemClick: (MarketsListItemUM) -> Unit, val visibleIdsChanged: (List) -> Unit, + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, ) : SwapMarketState() - data object Loading : SwapMarketState() + data class Loading( + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, + ) : SwapMarketState() data class LoadingError( val onRetryClicked: () -> Unit, + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, ) : SwapMarketState() - data object SearchNothingFound : SwapMarketState() + data object SearchNothingFound : SwapMarketState() { + override val marketsTitle: TextReference = TextReference.Res(R.string.markets_common_title) + override val shouldAssetsCount: Boolean = true + } } \ No newline at end of file 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 d140ef1ff0..7e5209dca5 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 @@ -132,6 +132,18 @@ internal object SwapNotificationUM { ), ) + data class SwapNotSupported( + val tokenName: String, + ) : Warning( + title = resourceReference( + id = com.tangem.feature.swap.presentation.R.string.express_swap_not_supported_title, + formatArgs = wrappedList(tokenName), + ), + subtitle = resourceReference( + com.tangem.feature.swap.presentation.R.string.express_swap_not_supported_text, + ), + ) + data class NeedReserveToCreateAccount( val receiveAmount: String, val receiveToken: String, 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 c3a85fe555..6c955e7c86 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 @@ -200,6 +200,60 @@ internal class StateBuilder( ) } + fun createSwapNotSupportedState( + uiStateHolder: SwapStateHolder, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, + toAccount: Account.CryptoPortfolio?, + ): SwapStateHolder { + if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder + return uiStateHolder.copy( + sendCardData = SwapCardState.SwapCardData( + type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable), + amountTextFieldValue = null, + amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + token = fromToken, + tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, + coinId = uiStateHolder.sendCardData.coinId, + isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, + tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, + canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, + balance = fromToken.getFormattedAmount(isNeedSymbol = false), + networkIconRes = getActiveIconRes(fromToken.currency.network.rawId), + isBalanceHidden = isBalanceHiddenProvider(), + ), + receiveCardData = SwapCardState.SwapCardData( + type = TransactionCardType.ReadOnly( + accountTitleUM = getToCardAccountTitle(toAccount), + ), + amountTextFieldValue = TextFieldValue( + text = "0", + ), + amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + token = toToken, + tokenIconUrl = toToken.currency.iconUrl, + coinId = toToken.currency.network.backendId, + isNotNativeToken = toToken.currency is CryptoCurrency.Token, + tokenCurrency = toToken.currency.symbol, + canSelectAnotherToken = true, + balance = toToken.getFormattedAmount(isNeedSymbol = false), + networkIconRes = getActiveIconRes(toToken.currency.network.rawId), + isBalanceHidden = isBalanceHiddenProvider(), + ), + notifications = notificationsFactory.getSwapNotSupportedNotifications(toToken.currency.name), + fee = FeeItemState.Empty, + swapButton = SwapButton( + walletInteractionIcon = walletInterationIcon(userWalletProvider()), + isEnabled = false, + isHoldToConfirm = isHoldToConfirmEnabled, + onClick = { }, + ), + changeCardsButtonState = ChangeCardsButtonState.DISABLED, + providerState = ProviderState.Empty(), + priceImpact = PriceImpact.Empty(), + ) + } + @Suppress("LongParameterList") fun createQuotesLoadingState( uiStateHolder: SwapStateHolder, @@ -438,6 +492,7 @@ internal class StateBuilder( notification is SwapNotificationUM.Warning.ExpressError || notification is SwapNotificationUM.Warning.ExpressGeneralError || notification is SwapNotificationUM.Warning.NoAvailableTokensToSwap || + notification is SwapNotificationUM.Warning.SwapNotSupported || notification is SwapNotificationUM.Warning.NeedReserveToCreateAccount || notification is SwapNotificationUM.Info.PermissionNeeded } 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 2bc3cc5c61..a6ddf89de1 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 @@ -189,7 +189,7 @@ private fun ListOfTokensWithMarkets( state = lazyListState, ) { if (state.tokensListData !is TokenListUMData.EmptyList) { - assetsTitle(count = state.tokensListData.totalTokensCount) + assetsTitle(count = state.tokensListData.totalTokensCount, showCount = marketsState.shouldAssetsCount) } tokensListItems( @@ -197,10 +197,18 @@ private fun ListOfTokensWithMarkets( isBalanceHidden = state.isBalanceHidden, ) - if (state.tokensListData is TokenListUMData.EmptyList) { + tokensToSelectItems(state.availableTokens, state.onTokenSelected) + if (state.unavailableTokens.isNotEmpty()) { item { SpacerH12() } - } else { + tokensToSelectItems(state.unavailableTokens, state.onTokenSelected) + } + + val hasPortfolioContent = state.tokensListData !is TokenListUMData.EmptyList || + state.availableTokens.isNotEmpty() + if (hasPortfolioContent) { item { SpacerH32() } + } else { + item { SpacerH12() } } swapMarketsListItems(marketsState) @@ -242,13 +250,15 @@ private fun VisibleItemsTracker(lazyListState: LazyListState, marketState: SwapM } } -private fun LazyListScope.assetsTitle(count: Int) { +private fun LazyListScope.assetsTitle(count: Int, showCount: Boolean) { item(key = "assets_title") { Text( text = buildAnnotatedString { append(stringResourceSafe(R.string.swap_your_assets_title)) - withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { - append(" $count") + if (showCount) { + withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { + append(" $count") + } } }, style = TangemTheme.typography.h3, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt index 0f69e31f8b..ced4fbda3e 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt @@ -14,6 +14,7 @@ import com.tangem.common.ui.markets.MarketsListItem import com.tangem.common.ui.markets.MarketsListItemPlaceholder import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.market.state.SwapMarketState @@ -24,7 +25,7 @@ internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) { val totalCount = (state as? SwapMarketState.Content)?.total Text( text = buildAnnotatedString { - append(stringResourceSafe(R.string.markets_common_title)) + append(state.marketsTitle.resolveReference()) if (totalCount != null) { withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { append(" $totalCount") 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 b9a6ab2d40..8c03c69176 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 @@ -4,8 +4,10 @@ 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 @@ -35,6 +37,8 @@ internal class SwapSelectTokenPreviewProvider { onItemClick = { }, visibleIdsChanged = { }, total = TOTAL_ITEMS, + marketsTitle = TextReference.Res(R.string.feed_trending_now), + shouldAssetsCount = false, ) private fun createPreviewMarketItems() = listOf( From 8b1fb82721a7005f618a737d28a60d51a87817ef Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Feb 2026 15:47:12 +0300 Subject: [PATCH 062/106] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 c04b7ea0fc..c4ccee2282 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 @@ -65,7 +65,7 @@ }, { "name": "SWAP_MARKET_LIST_ENABLED", - "version": "undefined" + "version": "5.34" }, { "name": "EARN_BLOCK_ENABLED", From 1cdece6ae7daa9c9c396b12d6d7a228ce2b62089 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Feb 2026 15:51:29 +0300 Subject: [PATCH 063/106] Updated on 2026-08-14 --- .../tap/domain/sdk/impl/DefaultTangemSdkManager.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 8639981110..11a3ffb59b 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -126,7 +126,13 @@ internal class DefaultTangemSdkManager( } if (awaitInitialization) { - awaitAuthenticationManagerInitialization().needEnrollBiometrics + val manager = awaitAuthenticationManagerInitialization() + + if (manager.isInitialized) { + manager.needEnrollBiometrics + } else { + false + } } else { throw e } @@ -145,7 +151,11 @@ internal class DefaultTangemSdkManager( if (awaitInitialization) { val manager = awaitAuthenticationManagerInitialization() - manager.canAuthenticate || manager.needEnrollBiometrics + if (manager.isInitialized) { + manager.canAuthenticate || manager.needEnrollBiometrics + } else { + false + } } else { throw e } From d7a120fc862ca038c1e36b3da301d62474a8ed19 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Feb 2026 19:18:15 +0400 Subject: [PATCH 064/106] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 27 +- .../api/pay/models/response/OrderResponse.kt | 1 + .../datasource/local/visa/TangemPayStorage.kt | 5 +- data/visa/build.gradle.kts | 1 + .../tangem/data/pay/di/TangemPayDataModule.kt | 2 +- .../data/pay/entity/WithdrawStoreData.kt | 19 ++ .../DefaultCustomerOrderRepository.kt | 26 +- .../DefaultTangemPaySwapRepository.kt | 99 ------ .../DefaultTangemPayWithdrawRepository.kt | 301 ++++++++++++++++++ .../DefaultTangemPayWithdrawUseCase.kt | 7 +- .../data/pay/util/WithdrawStateConverter.kt | 22 ++ .../pay/util/WithdrawStoreDataConverter.kt | 22 ++ domain/visa/build.gradle.kts | 1 + .../domain/pay/TangemPayWithdrawState.kt | 14 + .../com/tangem/domain/pay/model/OrderData.kt | 6 + .../pay/repository/CustomerOrderRepository.kt | 6 +- ...tory.kt => TangemPayWithdrawRepository.kt} | 9 +- .../TangemPayMainScreenCustomerInfoUseCase.kt | 12 +- .../tangempay/TangemPayWithdrawUseCase.kt | 2 + features/swap/domain/build.gradle.kts | 1 + .../feature/swap/domain/SwapInteractorImpl.kt | 9 + .../domain/models/ui/SwapTransactionState.kt | 2 + .../tangem/feature/swap/model/SwapModel.kt | 1 + .../tangempay/model/TangemPayDetailsModel.kt | 51 +-- 24 files changed, 486 insertions(+), 160 deletions(-) create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/entity/WithdrawStoreData.kt delete mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStateConverter.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStoreDataConverter.kt create mode 100644 domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayWithdrawState.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderData.kt rename domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/{TangemPaySwapRepository.kt => TangemPayWithdrawRepository.kt} (58%) 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 4dc9cefd1a..5708a79d8b 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -2,6 +2,9 @@ package com.tangem.tap.data import android.content.Context import com.squareup.moshi.Moshi +import com.tangem.data.pay.entity.WithdrawStoreData +import com.tangem.data.pay.util.WithdrawStateConverter +import com.tangem.data.pay.util.WithdrawStoreDataConverter import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -11,6 +14,7 @@ import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.TangemPayWithdrawState import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -41,6 +45,8 @@ internal class DefaultTangemPayStorage @Inject constructor( } private val tokensAdapter by lazy { moshi.adapter(TangemPayAuthTokens::class.java) } + private val withdrawStoreDataConverter by lazy { WithdrawStoreDataConverter() } + private val withdrawStateConverter by lazy { WithdrawStateConverter() } override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) { withContext(dispatcherProvider.io) { @@ -131,10 +137,12 @@ internal class DefaultTangemPayStorage @Inject constructor( return appPreferencesStore.getSyncOrNull(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId)) } - override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String) { + override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) { appPreferencesStore.editData { mutablePreferences -> - val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY) - .plus(createWithdrawOrderIdKey(userWalletId) to orderId) + val orders = mutablePreferences.getObjectMap( + PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, + ) + .plus(createWithdrawOrderIdKey(userWalletId) to withdrawStoreDataConverter.convert(data)) mutablePreferences.setObjectMap( key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, value = orders, @@ -142,14 +150,19 @@ internal class DefaultTangemPayStorage @Inject constructor( } } - override suspend fun getWithdrawOrderId(userWalletId: UserWalletId): String? { - val orders = appPreferencesStore.getObjectMapSync(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY) - return orders[createWithdrawOrderIdKey(userWalletId)] + override suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState? { + val orders = appPreferencesStore.getObjectMapSync( + PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, + ) + val data = orders[createWithdrawOrderIdKey(userWalletId)] ?: return null + return withdrawStateConverter.convert(data) } override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) { appPreferencesStore.editData { mutablePreferences -> - val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY) + val orders = mutablePreferences.getObjectMap( + PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, + ) .minus(createWithdrawOrderIdKey(userWalletId)) mutablePreferences.setObjectMap( key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt index a476b5dc1b..48cc8fc4fa 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt @@ -27,6 +27,7 @@ data class OrderResponse( @Json(name = "emboss_name") val embossName: String?, @Json(name = "product_instance_id") val productInstanceId: String?, @Json(name = "payment_account_id") val paymentAccountId: String?, + @Json(name = "transaction_hash") val transactionHash: String?, ) } } \ No newline at end of file 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 591bc9f6be..bd29637e98 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 @@ -1,6 +1,7 @@ package com.tangem.datasource.local.visa import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.TangemPayWithdrawState import com.tangem.domain.visa.model.TangemPayAuthTokens @Suppress("TooManyFunctions") @@ -28,9 +29,9 @@ interface TangemPayStorage { suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? - suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String) + suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) - suspend fun getWithdrawOrderId(userWalletId: UserWalletId): String? + suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState? suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 3c9ef9ff07..67768d9958 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(projects.domain.walletManager) implementation(projects.domain.quotes) implementation(projects.domain.common) + implementation(projects.features.swap.domain) /** Feature API - remove after removing [HotWalletFeatureToggles] */ implementation(projects.features.hotWallet.api) 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 500e5fa50d..5868e4c0c6 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 @@ -45,7 +45,7 @@ internal interface TangemPayDataModule { @Binds @Singleton - fun bindTangemPaySwapRepository(repository: DefaultTangemPaySwapRepository): TangemPaySwapRepository + fun bindTangemPaySwapRepository(repository: DefaultTangemPayWithdrawRepository): TangemPayWithdrawRepository @Binds @Singleton diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/entity/WithdrawStoreData.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/entity/WithdrawStoreData.kt new file mode 100644 index 0000000000..cdad87d12b --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/entity/WithdrawStoreData.kt @@ -0,0 +1,19 @@ +package com.tangem.data.pay.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +data class WithdrawStoreData( + @Json(name = "orderId") val orderId: String, + @Json(name = "exchangeData") val exchangeData: ExchangeStoreData?, +) + +@JsonClass(generateAdapter = false) +data class ExchangeStoreData( + @Json(name = "txId") val txId: String, + @Json(name = "fromNetwork") val fromNetwork: String, + @Json(name = "fromAddress") val fromAddress: String, + @Json(name = "payInAddress") val payInAddress: String, + @Json(name = "payInExtraId") val payInExtraId: String?, +) \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt index 11afd970e8..f479794766 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt @@ -2,8 +2,8 @@ package com.tangem.data.pay.repository import arrow.core.Either import com.tangem.datasource.api.pay.TangemPayApi -import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.model.OrderData import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.visa.error.VisaApiError @@ -12,35 +12,23 @@ import javax.inject.Inject internal class DefaultCustomerOrderRepository @Inject constructor( private val tangemPayApi: TangemPayApi, private val requestHelper: TangemPayRequestPerformer, - private val tangemPayStorage: TangemPayStorage, ) : CustomerOrderRepository { - override suspend fun getOrderStatus( - userWalletId: UserWalletId, - orderId: String, - ): Either { + override suspend fun getOrderData(userWalletId: UserWalletId, orderId: String): Either { return requestHelper.performRequest(userWalletId) { authHeader -> tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId) }.map { response -> - when (response.result?.status) { + val status = when (response.result?.status) { null -> OrderStatus.UNKNOWN OrderStatus.NEW.apiName -> OrderStatus.NEW OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED else -> OrderStatus.CANCELED } + OrderData( + status = status, + withdrawTxHash = response.result?.data?.transactionHash?.ifEmpty { null }, + ) } } - - override suspend fun hasWithdrawOrder(userWalletId: UserWalletId): Boolean { - val orderId = tangemPayStorage.getWithdrawOrderId(userWalletId) - if (orderId == null) return false - - val status = getOrderStatus(userWalletId, orderId).getOrNull() - - val hasActiveOrder = status == OrderStatus.NEW || status == OrderStatus.PROCESSING - if (!hasActiveOrder) tangemPayStorage.deleteWithdrawOrder(userWalletId) - - return hasActiveOrder - } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt deleted file mode 100644 index 6bc1774f28..0000000000 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.tangem.data.pay.repository - -import arrow.core.Either -import arrow.core.left -import com.tangem.core.error.UniversalError -import com.tangem.data.common.quote.QuotesFetcher -import com.tangem.datasource.api.pay.TangemPayApi -import com.tangem.datasource.api.pay.models.request.WithdrawDataRequest -import com.tangem.datasource.api.pay.models.request.WithdrawRequest -import com.tangem.datasource.local.visa.TangemPayStorage -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.pay.WithdrawalResult -import com.tangem.domain.pay.WithdrawalSignatureResult -import com.tangem.domain.pay.datasource.TangemPayAuthDataSource -import com.tangem.domain.pay.repository.TangemPaySwapRepository -import com.tangem.domain.visa.error.VisaApiError -import com.tangem.utils.extensions.addHexPrefix -import java.math.BigDecimal -import java.math.RoundingMode -import java.util.Currency -import java.util.Locale -import javax.inject.Inject - -@Suppress("LongParameterList") -internal class DefaultTangemPaySwapRepository @Inject constructor( - private val tangemPayApi: TangemPayApi, - private val requestHelper: TangemPayRequestPerformer, - private val authDataSource: TangemPayAuthDataSource, - private val quotesFetcher: QuotesFetcher, - private val tangemPayStorage: TangemPayStorage, -) : TangemPaySwapRepository { - - override suspend fun withdraw( - userWallet: UserWallet, - receiverAddress: String, - cryptoAmount: BigDecimal, - cryptoCurrencyId: CryptoCurrency.RawID, - ): Either { - val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId) - if (amountInCents.isNullOrEmpty()) return Either.Left(VisaApiError.WithdrawalDataError) - return requestHelper.performRequest(userWallet.walletId) { authHeader -> - val request = WithdrawDataRequest(amountInCents = amountInCents, recipientAddress = receiverAddress) - tangemPayApi.getWithdrawData(authHeader = authHeader, body = request) - }.map { data -> - val result = data.result ?: return VisaApiError.WithdrawalDataError.left() - val signatureResult = authDataSource.getWithdrawalSignature( - userWallet = userWallet, - hash = result.hash, - ).getOrNull() - - return when (signatureResult) { - is WithdrawalSignatureResult.Cancelled -> { - Either.Right(WithdrawalResult.Cancelled) - } - is WithdrawalSignatureResult.Success -> { - requestHelper.performRequest(userWallet.walletId) { authHeader -> - val request = WithdrawRequest( - amountInCents = amountInCents, - recipientAddress = receiverAddress, - adminSalt = result.salt, - senderAddress = result.senderAddress, - adminSignature = signatureResult.signature.addHexPrefix(), - ) - tangemPayApi.withdraw(authHeader = authHeader, body = request) - } - .mapLeft { return Either.Left(VisaApiError.WithdrawError) } - .map { response -> - val orderId = response.result?.orderId - if (orderId != null) tangemPayStorage.storeWithdrawOrder(userWallet.walletId, orderId) - WithdrawalResult.Success - } - } - null -> return Either.Left(VisaApiError.SignWithdrawError) - } - } - } - - private suspend fun getAmountInCents(cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID): String? { - val fiatRate = getFiatRate(cryptoCurrencyId) ?: return null - val amountInDollars = cryptoAmount.multiply(fiatRate) - val defaultFractionDigits = Currency.getInstance(Locale.US).defaultFractionDigits - return amountInDollars - .setScale(defaultFractionDigits, RoundingMode.HALF_UP) - .movePointRight(defaultFractionDigits) - .longValueExact() - .toString() - } - - private suspend fun getFiatRate(cryptoCurrencyId: CryptoCurrency.RawID): BigDecimal? { - val quotes = quotesFetcher.fetch( - fiatCurrencyId = Currency.getInstance(Locale.US).currencyCode, - currencyId = cryptoCurrencyId.value, - field = QuotesFetcher.Field.PRICE, - ).getOrNull() - - return quotes?.quotes[cryptoCurrencyId.value]?.price - } -} \ No newline at end of file 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 new file mode 100644 index 0000000000..cea8e61d60 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt @@ -0,0 +1,301 @@ +package com.tangem.data.pay.repository + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.core.error.UniversalError +import com.tangem.data.common.quote.QuotesFetcher +import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.api.pay.models.request.WithdrawDataRequest +import com.tangem.datasource.api.pay.models.request.WithdrawRequest +import com.tangem.datasource.api.pay.models.response.WithdrawResponse +import com.tangem.datasource.local.visa.TangemPayStorage +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.TangemPayWithdrawExchangeState +import com.tangem.domain.pay.TangemPayWithdrawState +import com.tangem.domain.pay.WithdrawalResult +import com.tangem.domain.pay.WithdrawalSignatureResult +import com.tangem.domain.pay.datasource.TangemPayAuthDataSource +import com.tangem.domain.pay.model.OrderData +import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.repository.CustomerOrderRepository +import com.tangem.domain.pay.repository.TangemPayWithdrawRepository +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.feature.swap.domain.api.SwapRepository +import com.tangem.feature.swap.domain.models.ExpressDataError +import com.tangem.utils.extensions.addHexPrefix +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +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 +import java.util.Locale +import javax.inject.Inject +import kotlin.collections.set +import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration.Companion.seconds + +private const val TAG = "TangemPaySwapRepository" + +@Suppress("LongParameterList") +internal class DefaultTangemPayWithdrawRepository @Inject constructor( + private val tangemPayApi: TangemPayApi, + private val requestHelper: TangemPayRequestPerformer, + private val authDataSource: TangemPayAuthDataSource, + private val quotesFetcher: QuotesFetcher, + private val tangemPayStorage: TangemPayStorage, + private val swapRepository: SwapRepository, + private val orderRepository: CustomerOrderRepository, +) : TangemPayWithdrawRepository { + + private val withdrawPollingScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val withdrawPollingJobs = mutableMapOf() + private val withdrawPollingMutex = Mutex() + + override suspend fun withdraw( + userWallet: UserWallet, + receiverAddress: String, + cryptoAmount: BigDecimal, + cryptoCurrencyId: CryptoCurrency.RawID, + exchangeData: TangemPayWithdrawExchangeState, + ): Either { + val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId) + if (amountInCents.isNullOrEmpty()) return Either.Left(VisaApiError.WithdrawalDataError) + return requestHelper.performRequest(userWallet.walletId) { authHeader -> + val request = WithdrawDataRequest(amountInCents = amountInCents, recipientAddress = receiverAddress) + tangemPayApi.getWithdrawData(authHeader = authHeader, body = request) + }.map { data -> + val result = data.result ?: return VisaApiError.WithdrawalDataError.left() + val signatureResult = authDataSource.getWithdrawalSignature( + userWallet = userWallet, + hash = result.hash, + ).getOrNull() + + return when (signatureResult) { + is WithdrawalSignatureResult.Cancelled -> { + Either.Right(WithdrawalResult.Cancelled) + } + is WithdrawalSignatureResult.Success -> { + requestHelper.performRequest(userWallet.walletId) { authHeader -> + val request = WithdrawRequest( + amountInCents = amountInCents, + recipientAddress = receiverAddress, + adminSalt = result.salt, + senderAddress = result.senderAddress, + adminSignature = signatureResult.signature.addHexPrefix(), + ) + tangemPayApi.withdraw(authHeader = authHeader, body = request) + } + .mapLeft { return Either.Left(VisaApiError.WithdrawError) } + .map { response -> + processWithdrawResult(response, userWallet, exchangeData) + WithdrawalResult.Success + } + } + null -> return Either.Left(VisaApiError.SignWithdrawError) + } + } + } + + private suspend fun processWithdrawResult( + response: WithdrawResponse, + userWallet: UserWallet, + exchangeData: TangemPayWithdrawExchangeState, + ) { + val orderId = response.result?.orderId + if (orderId != null) { + val orderData = orderRepository + .getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + val withdrawTxHash = orderData?.withdrawTxHash + val storeData = TangemPayWithdrawState( + orderId = orderId, + exchangeData = exchangeData, + ) + if (orderData != null && !withdrawTxHash.isNullOrEmpty()) { + finalizeWithdraw( + userWallet = userWallet, + withdrawTxHash = withdrawTxHash, + orderId = orderId, + exchangeData = exchangeData, + order = orderData, + ).onLeft { + tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) + } + } else { + tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) + } + } + } + + private suspend fun finalizeWithdraw( + userWallet: UserWallet, + withdrawTxHash: String, + orderId: String, + exchangeData: TangemPayWithdrawExchangeState, + order: OrderData, + ): Either { + return swapRepository.exchangeSent( + userWallet = userWallet, + txId = exchangeData.txId, + fromNetwork = exchangeData.fromNetwork, + fromAddress = exchangeData.fromAddress, + payInAddress = exchangeData.payInAddress, + txHash = withdrawTxHash, + payInExtraId = exchangeData.payInExtraId, + ) + .onRight { + val isActive = order.status == OrderStatus.NEW || order.status == OrderStatus.PROCESSING + if (!isActive) { + tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId) + } else { + tangemPayStorage.storeWithdrawOrder( + userWalletId = userWallet.walletId, + data = TangemPayWithdrawState(orderId = orderId, exchangeData = null), + ) + } + } + .onLeft { error -> + Timber.tag(TAG).e(error.toString()) + } + } + + override suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean { + val orderExchangeData = tangemPayStorage.getWithdrawOrder(userWallet.walletId) + if (orderExchangeData == null) return false + + val exchangeData = orderExchangeData.exchangeData + val orderData = orderRepository + .getOrderData(userWallet.walletId, orderId = orderExchangeData.orderId).getOrNull() + val withdrawTxHash = orderData?.withdrawTxHash + + if (exchangeData != null && orderData != null && withdrawTxHash != null) { + finalizeWithdraw( + userWallet = userWallet, + withdrawTxHash = withdrawTxHash, + orderId = orderExchangeData.orderId, + exchangeData = exchangeData, + order = orderData, + ) + } + + return orderData?.status == OrderStatus.NEW || orderData?.status == OrderStatus.PROCESSING + } + + override suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either { + val storeData = tangemPayStorage.getWithdrawOrder(userWallet.walletId) ?: return Unit.right() + val exchangeData = storeData.exchangeData ?: return Unit.right() + + val orderId = storeData.orderId + val order = orderRepository + .getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + ?: return Unit.right() + + val txHash = order.withdrawTxHash + + if (!txHash.isNullOrEmpty()) { + finalizeWithdraw( + userWallet = userWallet, + withdrawTxHash = txHash, + orderId = storeData.orderId, + exchangeData = exchangeData, + order = order, + ).onLeft { + startWithdrawOrderPolling( + userWallet = userWallet, + orderId = orderId, + storeData = storeData, + exchangeData = exchangeData, + ) + } + } else { + startWithdrawOrderPolling( + userWallet = userWallet, + orderId = orderId, + storeData = storeData, + exchangeData = exchangeData, + ) + } + + return Unit.right() + } + + private suspend fun startWithdrawOrderPolling( + userWallet: UserWallet, + orderId: String, + storeData: TangemPayWithdrawState, + exchangeData: TangemPayWithdrawExchangeState, + ) { + withdrawPollingMutex.withLock { + if (withdrawPollingJobs.containsKey(orderId)) return + + val pollingJob = withdrawPollingScope.launch { + try { + while (isActive && withdrawPollingJobs.containsKey(orderId)) { + delay(duration = 5.seconds) + + val orderData = orderRepository + .getOrderData(userWalletId = userWallet.walletId, orderId = orderId) + orderData.onRight { order -> + if (order.status != OrderStatus.NEW && order.status != OrderStatus.PROCESSING) { + tangemPayStorage.deleteWithdrawOrder(userWallet.walletId) + withdrawPollingJobs.remove(key = orderId) + return@launch + } + val txHash = order.withdrawTxHash + if (!txHash.isNullOrEmpty()) { + finalizeWithdraw( + userWallet = userWallet, + withdrawTxHash = txHash, + orderId = storeData.orderId, + exchangeData = exchangeData, + order = order, + ).onRight { + withdrawPollingJobs.remove(key = orderId) + return@launch + } + } + }.onLeft { error -> + Timber.tag(TAG).e("error ${error.errorCode}") + } + } + } catch (exception: CancellationException) { + throw exception + } catch (exception: Exception) { + Timber.tag(TAG).e(exception) + withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } + } + } + withdrawPollingJobs[orderId] = pollingJob + } + } + + private suspend fun getAmountInCents(cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID): String? { + val fiatRate = getFiatRate(cryptoCurrencyId) ?: return null + val amountInDollars = cryptoAmount.multiply(fiatRate) + val defaultFractionDigits = Currency.getInstance(Locale.US).defaultFractionDigits + return amountInDollars + .setScale(defaultFractionDigits, RoundingMode.HALF_UP) + .movePointRight(defaultFractionDigits) + .longValueExact() + .toString() + } + + private suspend fun getFiatRate(cryptoCurrencyId: CryptoCurrency.RawID): BigDecimal? { + val quotes = quotesFetcher.fetch( + fiatCurrencyId = Currency.getInstance(Locale.US).currencyCode, + currencyId = cryptoCurrencyId.value, + field = QuotesFetcher.Field.PRICE, + ).getOrNull() + + return quotes?.quotes[cryptoCurrencyId.value]?.price + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt index 36b85985ad..73f569a219 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt @@ -4,14 +4,15 @@ import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.TangemPayWithdrawExchangeState import com.tangem.domain.pay.WithdrawalResult -import com.tangem.domain.pay.repository.TangemPaySwapRepository +import com.tangem.domain.pay.repository.TangemPayWithdrawRepository import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import java.math.BigDecimal import javax.inject.Inject internal class DefaultTangemPayWithdrawUseCase @Inject constructor( - private val repository: TangemPaySwapRepository, + private val repository: TangemPayWithdrawRepository, ) : TangemPayWithdrawUseCase { override suspend fun invoke( @@ -19,12 +20,14 @@ internal class DefaultTangemPayWithdrawUseCase @Inject constructor( cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, receiverCexAddress: String, + exchangeData: TangemPayWithdrawExchangeState, ): Either { return repository.withdraw( userWallet = userWallet, cryptoAmount = cryptoAmount, receiverAddress = receiverCexAddress, cryptoCurrencyId = cryptoCurrencyId, + exchangeData = exchangeData, ) } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStateConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStateConverter.kt new file mode 100644 index 0000000000..5bcefd3b8e --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStateConverter.kt @@ -0,0 +1,22 @@ +package com.tangem.data.pay.util + +import com.tangem.data.pay.entity.WithdrawStoreData +import com.tangem.domain.pay.TangemPayWithdrawExchangeState +import com.tangem.domain.pay.TangemPayWithdrawState +import com.tangem.utils.converter.Converter + +class WithdrawStateConverter : Converter { + + override fun convert(value: WithdrawStoreData): TangemPayWithdrawState = TangemPayWithdrawState( + orderId = value.orderId, + exchangeData = value.exchangeData?.let { exchangeData -> + TangemPayWithdrawExchangeState( + txId = exchangeData.txId, + fromNetwork = exchangeData.fromNetwork, + fromAddress = exchangeData.fromAddress, + payInAddress = exchangeData.payInAddress, + payInExtraId = exchangeData.payInExtraId, + ) + }, + ) +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStoreDataConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStoreDataConverter.kt new file mode 100644 index 0000000000..e46044b735 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStoreDataConverter.kt @@ -0,0 +1,22 @@ +package com.tangem.data.pay.util + +import com.tangem.data.pay.entity.ExchangeStoreData +import com.tangem.data.pay.entity.WithdrawStoreData +import com.tangem.domain.pay.TangemPayWithdrawState +import com.tangem.utils.converter.Converter + +class WithdrawStoreDataConverter : Converter { + + override fun convert(value: TangemPayWithdrawState): WithdrawStoreData = WithdrawStoreData( + orderId = value.orderId, + exchangeData = value.exchangeData?.let { exchangeData -> + ExchangeStoreData( + txId = exchangeData.txId, + fromNetwork = exchangeData.fromNetwork, + fromAddress = exchangeData.fromAddress, + payInAddress = exchangeData.payInAddress, + payInExtraId = exchangeData.payInExtraId, + ) + }, + ) +} \ No newline at end of file diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index 3dfad027a7..559774d3e2 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(projects.domain.core) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.features.swap.domain) /** Feature API - remove after removing [TangemPayFeatureToggles] */ implementation(projects.features.tangempay.details.api) diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayWithdrawState.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayWithdrawState.kt new file mode 100644 index 0000000000..a1424c3acd --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayWithdrawState.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.pay + +data class TangemPayWithdrawState( + val orderId: String, + val exchangeData: TangemPayWithdrawExchangeState?, +) + +data class TangemPayWithdrawExchangeState( + val txId: String, + val fromNetwork: String, + val fromAddress: String, + val payInAddress: String, + val payInExtraId: String?, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderData.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderData.kt new file mode 100644 index 0000000000..42b7001564 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderData.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.pay.model + +data class OrderData( + val status: OrderStatus, + val withdrawTxHash: String?, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt index 9ae1fd414b..09b25edc3a 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt @@ -2,12 +2,10 @@ package com.tangem.domain.pay.repository import arrow.core.Either import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.model.OrderData import com.tangem.domain.visa.error.VisaApiError interface CustomerOrderRepository { - suspend fun getOrderStatus(userWalletId: UserWalletId, orderId: String): Either - - suspend fun hasWithdrawOrder(userWalletId: UserWalletId): Boolean + suspend fun getOrderData(userWalletId: UserWalletId, orderId: String): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt similarity index 58% rename from domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt rename to domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt index fed66aab7d..18b09d331e 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt @@ -4,15 +4,22 @@ import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.TangemPayWithdrawExchangeState import com.tangem.domain.pay.WithdrawalResult +import com.tangem.domain.visa.error.VisaApiError import java.math.BigDecimal -interface TangemPaySwapRepository { +interface TangemPayWithdrawRepository { suspend fun withdraw( userWallet: UserWallet, receiverAddress: String, cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, + exchangeData: TangemPayWithdrawExchangeState, ): Either + + suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean + + suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either } \ 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 7ff408d1c1..647e6ad412 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 @@ -137,13 +137,13 @@ class TangemPayMainScreenCustomerInfoUseCase( userWalletId: UserWalletId, orderId: String, ): Either { - return customerOrderRepository.getOrderStatus(userWalletId, orderId = orderId) + return customerOrderRepository.getOrderData(userWalletId, orderId = orderId) .fold( ifLeft = { error -> error.mapErrorForCustomer().left() }, - ifRight = { orderStatus -> - when (orderStatus) { + ifRight = { orderData -> + when (orderData.status) { // Kyc is passed and user waits for order creation -> no need to get customer info OrderStatus.NEW, OrderStatus.PROCESSING, @@ -154,7 +154,7 @@ class TangemPayMainScreenCustomerInfoUseCase( kycStatus = CustomerInfo.KycStatus.APPROVED, cardInfo = null, ), - orderStatus = orderStatus, + orderStatus = orderData.status, ).right() // Order was created/cancelled -> clear order id and get customer info @@ -164,11 +164,11 @@ class TangemPayMainScreenCustomerInfoUseCase( -> { onboardingRepository.clearOrderId(userWalletId) // If order was cancelled -> start order creation - if (orderStatus == OrderStatus.CANCELED) onboardingRepository.createOrder(userWalletId) + if (orderData.status == OrderStatus.CANCELED) onboardingRepository.createOrder(userWalletId) onboardingRepository.getCustomerInfo(userWalletId = userWalletId) .mapLeft { it.mapErrorForCustomer() } .map { customerInfo -> - MainScreenCustomerInfo(info = customerInfo, orderStatus = orderStatus) + MainScreenCustomerInfo(info = customerInfo, orderStatus = orderData.status) } } } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt index e2abfd227f..d98264599f 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.TangemPayWithdrawExchangeState import com.tangem.domain.pay.WithdrawalResult import java.math.BigDecimal @@ -14,5 +15,6 @@ interface TangemPayWithdrawUseCase { cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, receiverCexAddress: String, + exchangeData: TangemPayWithdrawExchangeState, ): Either } \ No newline at end of file diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index b74d66f7e2..02e0992d2b 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(projects.domain.express.models) implementation(projects.domain.account) implementation(projects.domain.account.status) + implementation(projects.domain.visa.models) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) 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 060390b7a6..cbaa2d6509 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 @@ -36,6 +36,7 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.TangemPayWithdrawExchangeState import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.tokens.* @@ -1030,6 +1031,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.Error.UnknownError if (isTangemPayWithdrawal) { + val networkAddress = currencyToSend.value.networkAddress return SwapTransactionState.TangemPayWithdrawalData( cryptoAmount = amount.value, cryptoCurrencyId = requireNotNull(currencyToSend.currency.id.rawCurrencyId), @@ -1056,6 +1058,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( txExternalId = exchangeDataCex.externalTxId, averageDuration = null, ), + exchangeData = TangemPayWithdrawExchangeState( + txId = exchangeDataCex.txId, + fromNetwork = currencyToSend.currency.network.backendId, + fromAddress = networkAddress?.defaultAddress?.value.orEmpty(), + payInAddress = exchangeData.transaction.txTo, + payInExtraId = exchangeDataCex.txExtraId, + ), ) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt index 4619adf9ab..b1380f6a3a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt @@ -3,6 +3,7 @@ package com.tangem.feature.swap.domain.models.ui 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.pay.TangemPayWithdrawExchangeState import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount @@ -31,6 +32,7 @@ sealed class SwapTransactionState { val toAmount: String?, val toAmountValue: BigDecimal?, val storeData: StoreTransactionData, + val exchangeData: TangemPayWithdrawExchangeState, ) : SwapTransactionState() { data class StoreTransactionData( 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 a90bd3640a..fb4e74e1ff 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 @@ -1136,6 +1136,7 @@ internal class SwapModel @Inject constructor( cryptoAmount = swapTransactionState.cryptoAmount, cryptoCurrencyId = swapTransactionState.cryptoCurrencyId, receiverCexAddress = swapTransactionState.cexAddress, + exchangeData = swapTransactionState.exchangeData, ) .onLeft { startLoadingQuotesFromLastState() 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 aa37f99d4a..793549861d 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 @@ -26,8 +26,8 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayTopUpData -import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.domain.pay.repository.TangemPayWithdrawRepository import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.domain.visa.model.TangemPayTxHistoryItem @@ -79,7 +79,7 @@ internal class TangemPayDetailsModel @Inject constructor( private val cardDetailsEventListener: CardDetailsEventListener, private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener, private val tangemPayCryptoCurrencyFactory: TangemPayCryptoCurrencyFactory, - private val orderRepository: CustomerOrderRepository, + private val tangemPayWithdrawRepository: TangemPayWithdrawRepository, private val getUserWalletUseCase: GetUserWalletUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val expressTransactionsEventListener: ExpressTransactionsEventListener, @@ -124,6 +124,7 @@ internal class TangemPayDetailsModel @Inject constructor( modelScope.launch { expressTransactionsEventListener.send(ExpressTransactionsEvent.Update) } + subscribeToWithdrawOrder() } fun onPause() { @@ -148,6 +149,14 @@ internal class TangemPayDetailsModel @Inject constructor( .launchIn(modelScope) } + private fun subscribeToWithdrawOrder() { + modelScope.launch { + val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull() + ?: return@launch + tangemPayWithdrawRepository.pollWithdrawOrderIfNeeds(userWallet) + } + } + override fun onClickPinCode() { analytics.send(TangemPayAnalyticsEvents.PinCodeClicked()) if (!params.config.isPinSet) { @@ -278,24 +287,28 @@ internal class TangemPayDetailsModel @Inject constructor( if (currentBalance == null || depositAddress == null) { showBottomSheetError(TangemPayDetailsErrorType.Withdraw) } else { - modelScope.launch { - val hasActiveWithdrawal = orderRepository.hasWithdrawOrder(userWalletId = params.userWalletId) - if (hasActiveWithdrawal) { - showBottomSheetError(TangemPayDetailsErrorType.WithdrawInProgress) - } else { - val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull() - val currency = cryptoCurrency ?: userWallet?.let { - tangemPayCryptoCurrencyFactory.create(userWallet = userWallet, chainId = params.config.chainId) - .getOrNull() - } - if (currency != null) { - uiMessageSender.send( - message = TangemPayMessagesFactory.createWithdrawWarning( - onGotItClick = { onConfirmWithdrawal(currency, currentBalance, depositAddress) }, - ), - ) + val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull() + if (userWallet == null) { + showBottomSheetError(TangemPayDetailsErrorType.Withdraw) + } else { + modelScope.launch { + val hasActiveWithdrawal = tangemPayWithdrawRepository.hasWithdrawOrder(userWallet = userWallet) + if (hasActiveWithdrawal) { + showBottomSheetError(TangemPayDetailsErrorType.WithdrawInProgress) } else { - showBottomSheetError(TangemPayDetailsErrorType.Withdraw) + val currency = cryptoCurrency ?: tangemPayCryptoCurrencyFactory.create( + userWallet = userWallet, + chainId = params.config.chainId, + ).getOrNull() + if (currency != null) { + uiMessageSender.send( + message = TangemPayMessagesFactory.createWithdrawWarning( + onGotItClick = { onConfirmWithdrawal(currency, currentBalance, depositAddress) }, + ), + ) + } else { + showBottomSheetError(TangemPayDetailsErrorType.Withdraw) + } } } } From bd14f694c5afe474a33b468bbc57cbef2dd6ee15 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Feb 2026 11:25:36 +0400 Subject: [PATCH 065/106] Updated on 2026-08-14 --- .../SwapNotificationsComponent.kt | 6 +++++ .../model/SwapNotificationsModel.kt | 26 +++++++++++++++++++ .../confirm/SendWithSwapConfirmComponent.kt | 4 +++ .../confirm/model/SendWithSwapConfirmModel.kt | 4 +++ 4 files changed, 40 insertions(+) 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 8de4dc6649..3634569cf9 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 @@ -7,6 +7,8 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel 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.notifications.model.SwapNotificationsModel import com.tangem.features.swap.v2.impl.notifications.ui.swapNotifications import kotlinx.collections.immutable.ImmutableList @@ -40,6 +42,10 @@ internal class SwapNotificationsComponent( data class SwapNotificationData( val expressError: ExpressError?, val fromCryptoCurrency: CryptoCurrency?, + val destinationAddress: String, + val memo: String? = null, + val toCryptoCurrencyStatus: CryptoCurrencyStatus? = null, + val userWalletId: UserWalletId? = null, ) } } \ 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 e2be8113cd..89e743866c 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 @@ -1,5 +1,6 @@ package com.tangem.features.swap.v2.impl.notifications.model +import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -7,6 +8,8 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format 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.notifications.DefaultSwapNotificationsUpdateTrigger import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent @@ -14,6 +17,7 @@ import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent 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 @@ -30,6 +34,7 @@ internal class SwapNotificationsModel @Inject constructor( private val swapNotificationsUpdateListener: SwapNotificationsUpdateListener, private val swapNotificationsUpdateTrigger: DefaultSwapNotificationsUpdateTrigger, private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger, + private val validateTransactionUseCase: ValidateTransactionUseCase, paramsContainer: ParamsContainer, ) : Model() { @@ -61,12 +66,33 @@ internal class SwapNotificationsModel @Inject constructor( private suspend fun buildNotifications() { val notifications = buildList { addExpressErrorNotification() + addDestinationTagRequiredNotification() } swapNotificationsUpdateTrigger.callbackHasError(notifications.isNotEmpty()) uiState.value = notifications.toImmutableList() } + private suspend fun MutableList.addDestinationTagRequiredNotification() { + val toCryptoCurrencyStatus = notificationData.toCryptoCurrencyStatus ?: return + val userWalletId = notificationData.userWalletId ?: return + val destinationAddress = notificationData.destinationAddress + if (destinationAddress.isEmpty()) return + + val validationError = validateTransactionUseCase( + amount = BigDecimal.ZERO.convertToSdkAmount(toCryptoCurrencyStatus), + fee = null, + memo = notificationData.memo, + destination = destinationAddress, + userWalletId = userWalletId, + network = toCryptoCurrencyStatus.currency.network, + ).leftOrNull() + + if (validationError is BlockchainSdkError.DestinationTagRequired) { + add(NotificationUM.Error.DestinationTagRequired) + } + } + 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 aac3bb92dd..59d34c0701 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 @@ -133,6 +133,10 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( swapNotificationData = SwapNotificationsComponent.Params.SwapNotificationData( expressError = (model.confirmData.quote as? SwapQuoteUM.Error)?.expressError, fromCryptoCurrency = model.confirmData.fromCryptoCurrencyStatus?.currency, + destinationAddress = model.confirmData.enteredDestination.orEmpty(), + memo = model.confirmData.enteredMemo, + toCryptoCurrencyStatus = model.confirmData.toCryptoCurrencyStatus, + userWalletId = params.userWallet.walletId, ), ), ) 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 6fdf6bdb46..271decfaa7 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 @@ -429,6 +429,10 @@ internal class SendWithSwapConfirmModel @Inject constructor( data = SwapNotificationData( expressError = (confirmData.quote as? SwapQuoteUM.Error)?.expressError, fromCryptoCurrency = confirmData.fromCryptoCurrencyStatus?.currency, + destinationAddress = confirmData.enteredDestination.orEmpty(), + memo = confirmData.enteredMemo, + toCryptoCurrencyStatus = confirmData.toCryptoCurrencyStatus, + userWalletId = params.userWallet.walletId, ), ) uiState.transformerUpdate( From 7559207f595107fcfe2826bd092171bbb1fd0b34 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Feb 2026 11:25:50 +0400 Subject: [PATCH 066/106] Updated on 2026-08-14 --- .../wallets/analytics/WalletSettingsAnalyticEvents.kt | 4 ++++ .../features/details/model/UserWalletListModel.kt | 10 ++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt index be4e913124..850782d36a 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt @@ -171,6 +171,10 @@ sealed class WalletSettingsAnalyticEvents( event = "Wallet Upgraded", ), AppsFlyerIncludedEvent + class WalletsReorder : WalletSettingsAnalyticEvents( + event = "Longtap - Wallets Order", + ) + enum class RecoveryPhraseScreenAction(val value: String) { Upgrade("Upgrade"), Backup("Backup"), 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 5c011e92c8..44af704d49 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 @@ -13,6 +13,7 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.domain.wallets.usecase.ApplyUserWalletListSortingUseCase import com.tangem.domain.wallets.usecase.UnlockWalletUseCase import com.tangem.features.details.entity.UserWalletListUM @@ -138,10 +139,11 @@ internal class UserWalletListModel @Inject constructor( val userWalletIds = state.value.userWallets.map { UserWalletId(it.id) } modelScope.launch { - applyUserWalletListSortingUseCase(userWalletIds) - .onLeft { error -> - Timber.e("Failed to apply wallet list sorting: $error") - } + applyUserWalletListSortingUseCase(userWalletIds).onRight { + analyticsEventHandler.send(WalletSettingsAnalyticEvents.WalletsReorder()) + }.onLeft { error -> + Timber.e("Failed to apply wallet list sorting: $error") + } } } } \ No newline at end of file From 7f00a29920292af59d631e2bdf5caa2626e804c9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Feb 2026 19:21:55 +0400 Subject: [PATCH 067/106] Updated on 2026-08-14 --- .../main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt | 2 +- .../main/java/com/tangem/feature/swap/DefaultSwapRepository.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 20d6cbf213..b229533df3 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 @@ -248,7 +248,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( userWallet = userWallet, appPreferencesStore = appPreferencesStore, ), - toExtraId = toExtraId, + toExtraId = toExtraId?.ifEmpty { null }, ).getOrThrow() if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) { 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 d207e7992b..ef5dc2f371 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 @@ -312,7 +312,7 @@ internal class DefaultSwapRepository( userWallet = userWallet, appPreferencesStore = appPreferencesStore, ), - toExtraId = toExtraId, + toExtraId = toExtraId?.ifEmpty { null }, ).getOrThrow() if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) { val txDetails = parseTxDetails(response.txDetailsJson) From d0217583408efc22f681fcfd6c4918f663df2307 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 18 Feb 2026 14:58:21 +0700 Subject: [PATCH 068/106] Updated on 2026-08-14 --- .../wallet/presentation/wallet/domain/Wallet2CobrandImage.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt index 27856a0954..ae764cf797 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -251,7 +251,7 @@ internal enum class Wallet2CobrandImage( Sakura( cards2ResId = R.drawable.ill_sakura_card2_120_106, cards3ResId = R.drawable.ill_sakura_card3_120_106, - batchIds = setOf("AF990029", "AF990030", "AF990031"), + batchIds = setOf("AF990029", "AF990030", "AF990031", "AF990071", "AF990072", "AF990073"), ), SatoshiFriends( From a71a0e9a65f751105399744e34f5f5e57c8c49aa Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Feb 2026 10:51:15 +0300 Subject: [PATCH 069/106] Updated on 2026-08-14 --- .../account/createedit/ui/AccountCreateEditContent.kt | 6 +++++- .../features/onramp/tokenlist/ui/OnrampTokenList.kt | 2 +- .../com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt | 2 +- .../presentation/account/ExpandedAccountsHolder.kt | 9 +++++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt index 5d48231c07..792db8d675 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.vectorResource @@ -39,6 +40,7 @@ import com.tangem.core.ui.components.fields.AutoSizeTextField 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.utils.rememberHideKeyboardNestedScrollConnection import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.features.account.createedit.entity.AccountCreateEditUM import com.tangem.features.account.createedit.entity.AccountCreateEditUM.Account @@ -53,11 +55,12 @@ internal fun AccountCreateEditContent( ) { val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current + val nestedScrollConnection = rememberHideKeyboardNestedScrollConnection() + Column( modifier = modifier .background(color = TangemTheme.colors.background.secondary) .fillMaxSize() - .imePadding() .systemBarsPadding(), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -65,6 +68,7 @@ internal fun AccountCreateEditContent( Column( modifier = Modifier + .nestedScroll(nestedScrollConnection) .verticalScroll(rememberScrollState()) .padding(horizontal = 16.dp) .weight(1f), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index d92870b44f..e878df7dbd 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -213,7 +213,7 @@ internal fun LazyListScope.portfolioTokensList(portfolio: TokensListItemUM.Portf portfolioItem( portfolio = portfolio, - modifier = Modifier.padding(top = 8.dp), + modifier = Modifier, isBalanceHidden = isBalanceHidden, ) if (!isExpanded) return 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 a6ddf89de1..2a21dd13ec 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 @@ -322,7 +322,7 @@ internal fun LazyListScope.portfolioTokensList(portfolio: TokensListItemUM.Portf portfolioItem( portfolio = portfolio, - modifier = Modifier.padding(top = 8.dp), + modifier = Modifier, isBalanceHidden = isBalanceHidden, ) if (!isExpanded) return diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt index 59095ad0df..b1c48cff7e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt @@ -10,7 +10,9 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -43,10 +45,13 @@ internal class ExpandedAccountsHolder @Inject constructor( .toSet() // main state holder val expandedAccounts = MutableStateFlow(initExpandedState) + var debounceJob: Job? = null actionChannel .filter { (accountId, _) -> accountId.userWalletId == walletId } + .filter { debounceJob?.isActive != true } .onEach { (accountId, isExpand) -> + debounceJob = launch { delay(DEBOUNCE_MILLIS) } val newState = AccountExpandedState(accountId, isExpand) launch { accountsExpandedRepository.update(newState) } if (isExpand) { @@ -100,4 +105,8 @@ internal class ExpandedAccountsHolder @Inject constructor( } private fun walletAccounts(walletId: UserWalletId): Flow = singleAccountListSupplier(walletId) + + companion object { + private const val DEBOUNCE_MILLIS = 200L + } } \ No newline at end of file From 7b5b4753a7ef06b97e65d7e4dd35ce3eb85dea55 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Feb 2026 12:54:20 +0300 Subject: [PATCH 070/106] Updated on 2026-08-14 --- .../add/impl/model/CheckCurrencyUnsupportedDelegate.kt | 2 +- .../add/impl/model/CheckCurrencyUnsupportedDelegate.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 8ba89fb021..feb3025f69 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 @@ -66,7 +66,7 @@ class CheckCurrencyUnsupportedDelegate @Inject constructor( formatArgs = wrappedList(unsupportedState.networkName), ) is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, + id = R.string.alert_manage_tokens_unsupported_blockchain_by_card_message, formatArgs = wrappedList(unsupportedState.networkName), ) }, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt index a61f7cd842..f6d5e737b6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt @@ -66,7 +66,7 @@ class CheckCurrencyUnsupportedDelegate @Inject constructor( formatArgs = wrappedList(unsupportedState.networkName), ) is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, + id = R.string.alert_manage_tokens_unsupported_blockchain_by_card_message, formatArgs = wrappedList(unsupportedState.networkName), ) }, From 4e4e360ae184a93180d7531d2d8a82e4c9dc6d32 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 19 Feb 2026 11:10:14 +0400 Subject: [PATCH 071/106] Updated on 2026-08-14 --- .../DefaultWalletAddressServiceRepository.kt | 58 ++++++++----------- .../WalletAddressServiceRepository.kt | 2 +- .../usecase/ValidateWalletMemoUseCase.kt | 15 ++++- .../destination/model/SendDestinationModel.kt | 3 +- gradle/tangem_dependencies.toml | 2 +- 5 files changed, 39 insertions(+), 41 deletions(-) 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 49244f2da1..5c2bbe6654 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 @@ -1,12 +1,14 @@ package com.tangem.data.transaction import android.net.Uri -import androidx.core.text.isDigitsOnly import com.tangem.blockchain.blockchains.near.NearWalletManager 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 @@ -16,7 +18,6 @@ import com.tangem.domain.wallets.models.ParsedQrCode import com.tangem.domain.wallets.models.errors.ParsedQrCodeErrors import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext -import java.math.BigInteger class DefaultWalletAddressServiceRepository( private val walletManagersFacade: WalletManagersFacade, @@ -95,19 +96,28 @@ class DefaultWalletAddressServiceRepository( } } - override fun validateMemo(network: Network, memo: String): Boolean { - if (memo.isEmpty()) return true - return when (network.rawId) { - Blockchain.XRP.id -> { - val tag = memo.toLongOrNull() - tag != null && tag <= XRP_TAG_MAX_NUMBER + 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 + + 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 } - Blockchain.Stellar.id -> { - isAssignableXlmValue(memo) - } - else -> true } - } override suspend fun parseSharedAddress(input: String, network: Network): ParsedQrCode { val blockchain = network.toBlockchain() @@ -145,26 +155,4 @@ class DefaultWalletAddressServiceRepository( private fun Blockchain.isNear(): Boolean { return this == Blockchain.Near || this == Blockchain.NearTestnet } - - private fun isAssignableXlmValue(value: String): Boolean { - return when { - value.isNotEmpty() && value.isDigitsOnly() -> { - try { - // from com.tangem.blockchain.blockchains.stellar.StellarMemo.toStellarSdkMemo - value.toBigInteger() in BigInteger.ZERO..Long.MAX_VALUE.toBigInteger() * 2.toBigInteger() - } catch (ex: NumberFormatException) { - false - } - } - else -> { - // from org.stellar.sdk.MemoText - value.toByteArray().size <= XLM_MEMO_MAX_LENGTH - } - } - } - - companion object { - private const val XLM_MEMO_MAX_LENGTH = 28 - private const val XRP_TAG_MAX_NUMBER = 4294967295 - } } \ 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 f566a5a93b..e43d756df6 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,7 @@ interface WalletAddressServiceRepository { suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean - fun validateMemo(network: Network, memo: String): Boolean + suspend fun validateMemo(userWalletId: UserWalletId, network: Network, memo: 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 364eef4916..eb69e7feff 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 @@ -3,7 +3,8 @@ package com.tangem.domain.transaction.usecase import arrow.core.Either import arrow.core.left import arrow.core.right -import com.tangem.domain.models.network.Network +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.transaction.error.ValidateMemoError @@ -14,9 +15,17 @@ class ValidateWalletMemoUseCase( private val walletAddressServiceRepository: WalletAddressServiceRepository, ) { - operator fun invoke(network: Network, memo: String): Either { + suspend operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + memo: String, + ): Either { return try { - val isValidMemo = walletAddressServiceRepository.validateMemo(network, memo) + val isValidMemo = walletAddressServiceRepository.validateMemo( + userWalletId = userWalletId, + network = cryptoCurrency.network, + memo = memo, + ) if (isValidMemo) { Unit.right() } else { 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 95e67b5de3..96690cb647 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 @@ -323,8 +323,9 @@ internal class SendDestinationModel @Inject constructor( senderAddresses = senderAddresses.value, ) val memoValidationResult = validateWalletMemoUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, memo = memo.orEmpty(), - network = cryptoCurrency.network, ) if (type != null) { diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index a5765e9809..a0d719fc6b 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 = "releases-5.34-1428" +tangemBlockchainSdk = "releases-5.34-1430" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.34-579" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 9adbde6e55b0b5683ae2e39e3721b07deb630e63 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 19 Feb 2026 13:00:40 +0500 Subject: [PATCH 072/106] Updated on 2026-08-14 --- .../tap/di/domain/HotWalletDomainModule.kt | 27 ++ .../local/preferences/PreferencesKeys.kt | 4 - .../hotwallet/DefaultHotWalletRepository.kt | 29 +- domain/hot-wallet/build.gradle.kts | 6 + .../CheckHotWalletUpgradeBannerUseCase.kt | 61 +++++ .../CloseHotWalletUpgradeBannerUseCase.kt | 20 ++ ...ShouldShowUpgradeHotWalletBannerUseCase.kt | 12 + .../repository/HotWalletRepository.kt | 10 +- .../CheckHotWalletUpgradeBannerUseCaseTest.kt | 257 ++++++++++++++++++ .../CloseHotWalletUpgradeBannerUseCaseTest.kt | 39 +++ .../intents/WalletWarningsClickIntents.kt | 16 +- .../domain/GetMultiWalletWarningsFactory.kt | 48 +--- 12 files changed, 454 insertions(+), 75 deletions(-) create mode 100644 domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt create mode 100644 domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCase.kt create mode 100644 domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/ShouldShowUpgradeHotWalletBannerUseCase.kt create mode 100644 domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt create mode 100644 domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCaseTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt index 8c8404b2d3..c99860711a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt @@ -1,9 +1,12 @@ package com.tangem.tap.di.domain +import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase +import com.tangem.domain.hotwallet.CloseHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.hotwallet.IsAccessCodeSimpleUseCase import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase +import com.tangem.domain.hotwallet.ShouldShowUpgradeHotWalletBannerUseCase import com.tangem.domain.hotwallet.repository.HotWalletRepository import dagger.Module import dagger.Provides @@ -40,4 +43,28 @@ internal object HotWalletDomainModule { ): IsHotWalletCreationSupported { return IsHotWalletCreationSupported(hotWalletRepository) } + + @Provides + @Singleton + fun provideCheckHotWalletUpgradeBannerUseCase( + hotWalletRepository: HotWalletRepository, + ): CheckHotWalletUpgradeBannerUseCase { + return CheckHotWalletUpgradeBannerUseCase(hotWalletRepository) + } + + @Provides + @Singleton + fun provideCloseHotWalletUpgradeBannerUseCase( + hotWalletRepository: HotWalletRepository, + ): CloseHotWalletUpgradeBannerUseCase { + return CloseHotWalletUpgradeBannerUseCase(hotWalletRepository) + } + + @Provides + @Singleton + fun provideShouldShowUpgradeHotWalletBannerUseCase( + hotWalletRepository: HotWalletRepository, + ): ShouldShowUpgradeHotWalletBannerUseCase { + return ShouldShowUpgradeHotWalletBannerUseCase(hotWalletRepository) + } } \ No newline at end of file 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 eae141d861..e0c785a2e8 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 @@ -134,10 +134,6 @@ object PreferencesKeys { val SHOULD_SHOW_UPGRADE_BANNER_KEY by lazy { stringPreferencesKey(name = "shouldShowUpgradeBanner") } - val SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY by lazy { - stringPreferencesKey(name = "shouldShowNextTimeUpgradeBanner") - } - val UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY by lazy { stringPreferencesKey(name = "upgradeBannerClosureTimestamp") } val WALLET_CREATION_TIMESTAMP_KEY by lazy { stringPreferencesKey(name = "walletCreationTimestamp") } diff --git a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt index 686f94579d..dbbe66db29 100644 --- a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt +++ b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt @@ -10,11 +10,14 @@ import com.tangem.domain.hotwallet.repository.HotWalletRepository import com.tangem.domain.models.wallet.UserWalletId import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import java.util.concurrent.ConcurrentHashMap internal class DefaultHotWalletRepository( private val appPreferencesStore: AppPreferencesStore, ) : HotWalletRepository { + private val firstTopUpDetectedThisSession = ConcurrentHashMap() + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q) override fun isWalletCreationSupported(): Boolean { return BuildConfig.DEBUG || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q @@ -50,28 +53,12 @@ internal class DefaultHotWalletRepository( } } - override fun shouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId): Flow = appPreferencesStore - .getObjectMap(PreferencesKeys.SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY) - .map { it[userWalletId.stringValue] == true } - - override suspend fun setShouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) { - appPreferencesStore.editData { mutablePreferences -> - mutablePreferences.setObjectMap( - key = PreferencesKeys.SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY, - value = mutablePreferences.getObjectMap( - PreferencesKeys.SHOULD_SHOW_NEXT_TIME_UPGRADE_BANNER_KEY, - ) - .plus(userWalletId.stringValue to shouldShow), - ) - } - } - override suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long? { return appPreferencesStore .getObjectMapSync(PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY)[userWalletId.stringValue] } - override suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long) { + override suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long?) { appPreferencesStore.editData { mutablePreferences -> mutablePreferences.setObjectMap( key = PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY, @@ -110,4 +97,12 @@ internal class DefaultHotWalletRepository( ) } } + + override fun isFirstTopUpDetectedThisSession(userWalletId: UserWalletId): Boolean { + return firstTopUpDetectedThisSession.containsKey(userWalletId.stringValue) + } + + override fun markFirstTopUpDetectedThisSession(userWalletId: UserWalletId) { + firstTopUpDetectedThisSession[userWalletId.stringValue] = Unit + } } \ No newline at end of file diff --git a/domain/hot-wallet/build.gradle.kts b/domain/hot-wallet/build.gradle.kts index 471afb3452..621b607241 100644 --- a/domain/hot-wallet/build.gradle.kts +++ b/domain/hot-wallet/build.gradle.kts @@ -14,4 +14,10 @@ dependencies { implementation(projects.domain.wallets.models) implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) + + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) + testImplementation(deps.test.mockk) } \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt new file mode 100644 index 0000000000..1038773b7c --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.hotwallet + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import java.util.concurrent.TimeUnit + +class CheckHotWalletUpgradeBannerUseCase( + private val hotWalletRepository: HotWalletRepository, +) { + suspend operator fun invoke( + walletId: UserWalletId, + hasBalance: Boolean, + shouldShowUpgradeBanner: Boolean, + ): Either = try { + val currentTime = System.currentTimeMillis() + val creationTimestamp = hotWalletRepository.getWalletCreationTimestamp(walletId) + + val creationTimestampActual = if (creationTimestamp == null) { + // If creationTimestamp is null (wallet was created before this feature was released), + // store the current timestamp and use it below + hotWalletRepository.setWalletCreationTimestamp(walletId, currentTime) + currentTime + } else { + creationTimestamp + } + + val closureTimestamp = hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) + val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(walletId) + + val daysSinceCreation = TimeUnit.MILLISECONDS.toDays(currentTime - creationTimestampActual) + val daysSinceClosure = closureTimestamp?.let { TimeUnit.MILLISECONDS.toDays(currentTime - it) } + + // Wallet balance is positive, but the first top-up hasn't been tracked yet + if (hasBalance && !hasHadFirstTopUp) { + hotWalletRepository.setHasHadFirstTopUp(walletId, true) + hotWalletRepository.setShouldShowUpgradeBanner(walletId, true) + hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, null) + hotWalletRepository.markFirstTopUpDetectedThisSession(walletId) + } + + val shouldShow = when { + // Banner should be shown (e.g., because of the first top-up in the previous session) + shouldShowUpgradeBanner -> !hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) + // Banner was closed; it happened more than BANNER_RESHOW_DAYS (30) days ago + closureTimestamp != null && daysSinceClosure != null && daysSinceClosure >= BANNER_RESHOW_DAYS -> true + // Banner hasn't been closed; wallet was created more than BANNER_RESHOW_DAYS (30) days ago + closureTimestamp == null && daysSinceCreation >= BANNER_RESHOW_DAYS -> true + else -> false + } + shouldShow.right() + } catch (e: Exception) { + e.left() + } + + companion object { + const val BANNER_RESHOW_DAYS = 30L + } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCase.kt new file mode 100644 index 0000000000..17591a2bb1 --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.hotwallet + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId + +class CloseHotWalletUpgradeBannerUseCase( + private val hotWalletRepository: HotWalletRepository, +) { + suspend operator fun invoke(walletId: UserWalletId): Either = try { + val currentTime = System.currentTimeMillis() + hotWalletRepository.setShouldShowUpgradeBanner(walletId, false) + hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, currentTime) + Unit.right() + } catch (e: Exception) { + e.left() + } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/ShouldShowUpgradeHotWalletBannerUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/ShouldShowUpgradeHotWalletBannerUseCase.kt new file mode 100644 index 0000000000..49e17e6c7c --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/ShouldShowUpgradeHotWalletBannerUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.hotwallet + +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow + +class ShouldShowUpgradeHotWalletBannerUseCase( + private val hotWalletRepository: HotWalletRepository, +) { + operator fun invoke(userWalletId: UserWalletId): Flow = + hotWalletRepository.shouldShowUpgradeBanner(userWalletId) +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt index 2432bc3dc9..01229d8d3a 100644 --- a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt @@ -17,13 +17,9 @@ interface HotWalletRepository { suspend fun setShouldShowUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) - fun shouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId): Flow - - suspend fun setShouldShowNextTimeUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) - suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long? - suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long) + suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long?) suspend fun getWalletCreationTimestamp(userWalletId: UserWalletId): Long? @@ -32,4 +28,8 @@ interface HotWalletRepository { suspend fun hasHadFirstTopUp(userWalletId: UserWalletId): Boolean suspend fun setHasHadFirstTopUp(userWalletId: UserWalletId, hasTopUp: Boolean) + + fun isFirstTopUpDetectedThisSession(userWalletId: UserWalletId): Boolean + + fun markFirstTopUpDetectedThisSession(userWalletId: UserWalletId) } \ No newline at end of file diff --git a/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt new file mode 100644 index 0000000000..6e415efea4 --- /dev/null +++ b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt @@ -0,0 +1,257 @@ +package com.tangem.domain.hotwallet + +import arrow.core.Either +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.util.concurrent.TimeUnit + +class CheckHotWalletUpgradeBannerUseCaseTest { + + private val hotWalletRepository: HotWalletRepository = mockk(relaxed = true) + private val useCase = CheckHotWalletUpgradeBannerUseCase(hotWalletRepository) + + private val walletId = UserWalletId("0123456789ABCDEF") + + @Test + fun `GIVEN creation timestamp is null WHEN invoke THEN set timestamp and return false`() = runTest { + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns null + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + coVerify { hotWalletRepository.setWalletCreationTimestamp(walletId, any()) } + } + + @Test + fun `GIVEN shouldShowUpgradeBanner is true and hasBalance WHEN invoke THEN return true`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = true, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isTrue() + } + + @Test + fun `GIVEN shouldShowUpgradeBanner is true WHEN invoke THEN return true regardless of balance`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = true, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isTrue() + } + + @Test + fun `GIVEN closure timestamp exists and 30 days since closure WHEN invoke THEN return true`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) + val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isTrue() + } + + @Test + fun `GIVEN closure timestamp exists but less than 30 days since closure WHEN invoke THEN return false`() = + runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) + val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(15) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + } + + @Test + fun `GIVEN no flags set and no closure and 30 days since creation WHEN invoke THEN return true`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isTrue() + } + + @Test + fun `GIVEN no flags set but less than 30 days since creation WHEN invoke THEN return false`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(15) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + } + + @Test + fun `GIVEN no flags set but closure timestamp exists WHEN invoke THEN return false`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) + val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + } + + @Test + fun `GIVEN first top-up detected WHEN invoke THEN return false and mark session`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = false, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + coVerify { hotWalletRepository.setHasHadFirstTopUp(walletId, true) } + coVerify { hotWalletRepository.setShouldShowUpgradeBanner(walletId, true) } + coVerify { hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, null) } + verify { hotWalletRepository.markFirstTopUpDetectedThisSession(walletId) } + } + + @Test + fun `GIVEN first top-up detected this session WHEN invoke THEN return false`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns true + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = true, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + } + + @Test + fun `GIVEN already had first top-up WHEN invoke with balance THEN do not set flags again`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = true, + ) + + coVerify(exactly = 0) { hotWalletRepository.setHasHadFirstTopUp(any(), any()) } + coVerify(exactly = 0) { hotWalletRepository.setShouldShowUpgradeBanner(any(), any()) } + } + + @Test + fun `GIVEN multiple re-emissions with same state WHEN invoke THEN return same result`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result1 = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + val result2 = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + val result3 = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + ) + + assertThat((result1 as Either.Right).value).isTrue() + assertThat((result2 as Either.Right).value).isTrue() + assertThat((result3 as Either.Right).value).isTrue() + } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCaseTest.kt b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCaseTest.kt new file mode 100644 index 0000000000..d961d07617 --- /dev/null +++ b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCaseTest.kt @@ -0,0 +1,39 @@ +package com.tangem.domain.hotwallet + +import arrow.core.Either +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class CloseHotWalletUpgradeBannerUseCaseTest { + + private val hotWalletRepository: HotWalletRepository = mockk(relaxed = true) + private val useCase = CloseHotWalletUpgradeBannerUseCase(hotWalletRepository) + + private val walletId = UserWalletId("0123456789ABCDEF") + + @Test + fun `WHEN invoke THEN set banner flag to false and closure timestamp`() = runTest { + val result = useCase(walletId) + + assertThat(result).isInstanceOf(Either.Right::class.java) + coVerify { hotWalletRepository.setShouldShowUpgradeBanner(walletId, false) } + coVerify { hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, any()) } + } + + @Test + fun `GIVEN repository throws exception WHEN invoke THEN return Either Left`() = runTest { + val exception = RuntimeException("Test error") + coEvery { hotWalletRepository.setShouldShowUpgradeBanner(walletId, false) } throws exception + + val result = useCase(walletId) + + assertThat(result).isInstanceOf(Either.Left::class.java) + assertThat((result as Either.Left).value).isEqualTo(exception) + } +} \ 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 ca5a2d4caa..0cdab04aa3 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 @@ -17,7 +17,7 @@ import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.hotwallet.CloseHotWalletUpgradeBannerUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -135,7 +135,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase, private val uiMessageSender: UiMessageSender, private val reviewManager: ReviewManager, - private val hotWalletRepository: HotWalletRepository, + private val closeHotWalletUpgradeBannerUseCase: CloseHotWalletUpgradeBannerUseCase, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { @@ -554,8 +554,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( modelScope.launch(dispatchers.main) { val userWallet = getUserWalletUseCase(userWalletId).getOrNull() if (userWallet is UserWallet.Hot) { - hotWalletRepository.setShouldShowUpgradeBanner(userWalletId, false) - hotWalletRepository.setShouldShowNextTimeUpgradeBanner(userWalletId, false) appRouter.push(UpgradeWallet(userWalletId)) } } @@ -565,15 +563,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( modelScope.launch(dispatchers.main) { val userWallet = getUserWalletUseCase(userWalletId).getOrNull() if (userWallet is UserWallet.Hot) { - val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(userWalletId) - val currentTime = System.currentTimeMillis() - - if (hasHadFirstTopUp) { - hotWalletRepository.setShouldShowUpgradeBanner(userWalletId, false) - hotWalletRepository.setUpgradeBannerClosureTimestamp(userWalletId, currentTime) - } else { - hotWalletRepository.setShouldShowUpgradeBanner(userWalletId, true) - } + closeHotWalletUpgradeBannerUseCase(userWalletId) } } } 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 6f8e6b1be6..6218c6257a 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 @@ -13,8 +13,9 @@ import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase -import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.hotwallet.ShouldShowUpgradeHotWalletBannerUseCase import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrency @@ -44,7 +45,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map -import java.util.concurrent.TimeUnit + import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -60,7 +61,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val notificationsRepository: NotificationsRepository, private val accountDependencies: AccountDependencies, private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, - private val hotWalletRepository: HotWalletRepository, + private val shouldShowUpgradeHotWalletBannerUseCase: ShouldShowUpgradeHotWalletBannerUseCase, + private val checkHotWalletUpgradeBannerUseCase: CheckHotWalletUpgradeBannerUseCase, ) { @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod") @@ -101,8 +103,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(), shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.YieldPromo) .distinctUntilChanged(), - hotWalletRepository.shouldShowUpgradeBanner(userWallet.walletId).distinctUntilChanged(), - hotWalletRepository.shouldShowNextTimeUpgradeBanner(userWallet.walletId).distinctUntilChanged(), + shouldShowUpgradeHotWalletBannerUseCase.invoke(userWallet.walletId) + .distinctUntilChanged(), ) { array -> array } .combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = array) } .map { array -> @@ -117,7 +119,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val shouldAccessCodeSkipped = array[6] as Boolean val shouldShowYieldPromo = array[7] as Boolean val shouldShowUpgradeBanner = array[8] as Boolean - val shouldShowNextTimeUpgradeBanner = array[9] as Boolean buildList { addUsedOutdatedDataNotification(totalFiatBalance) @@ -129,7 +130,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( flattenCurrencies = flattenCurrencies, clickIntents = clickIntents, shouldShowUpgradeBanner = shouldShowUpgradeBanner, - shouldShowNextTimeUpgradeBanner = shouldShowNextTimeUpgradeBanner, ) addFinishWalletActivationNotification( @@ -464,45 +464,22 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - @Suppress("CyclomaticComplexMethod") private suspend fun MutableList.addUpgradeHotWalletPromoNotification( userWallet: UserWallet, flattenCurrencies: Lce>, clickIntents: WalletClickIntents, shouldShowUpgradeBanner: Boolean, - shouldShowNextTimeUpgradeBanner: Boolean, ) { if (userWallet !is UserWallet.Hot) return - val currentTime = System.currentTimeMillis() - val creationTimestamp = hotWalletRepository.getWalletCreationTimestamp(userWallet.walletId) - val closureTimestamp = hotWalletRepository.getUpgradeBannerClosureTimestamp(userWallet.walletId) - val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(userWallet.walletId) - - if (creationTimestamp == null) { - hotWalletRepository.setWalletCreationTimestamp(userWallet.walletId, currentTime) - return - } - - val daysSinceCreation = TimeUnit.MILLISECONDS.toDays(currentTime - creationTimestamp) - val daysSinceClosure = closureTimestamp?.let { TimeUnit.MILLISECONDS.toDays(currentTime - it) } - val currencies = flattenCurrencies.getOrNull(isPartialContentAccepted = true).orEmpty() val hasBalance = currencies.any { it.value.amount.orZero().isPositive() } - if (hasBalance && !hasHadFirstTopUp) { - hotWalletRepository.setHasHadFirstTopUp(userWallet.walletId, true) - } - - val shouldShow = when { - shouldShowUpgradeBanner && hasBalance -> true - shouldShowNextTimeUpgradeBanner && daysSinceClosure != null && daysSinceClosure >= UPGRADE_BANNER_RESHOW_DAYS -> true - !shouldShowUpgradeBanner && !shouldShowNextTimeUpgradeBanner && hasHadFirstTopUp && daysSinceCreation >= UPGRADE_BANNER_RESHOW_DAYS -> { - hotWalletRepository.setShouldShowNextTimeUpgradeBanner(userWallet.walletId, true) - true - } - else -> false - } + val shouldShow = checkHotWalletUpgradeBannerUseCase( + walletId = userWallet.walletId, + hasBalance = hasBalance, + shouldShowUpgradeBanner = shouldShowUpgradeBanner, + ).getOrNull() ?: return addIf( element = WalletNotification.UpgradeHotWalletPromo( @@ -515,6 +492,5 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private companion object { const val MAX_REMAINING_SIGNATURES_COUNT = 10 - const val UPGRADE_BANNER_RESHOW_DAYS = 30 } } \ No newline at end of file From 405912fd546b23554cba70fd43aaca970ed0f860 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 11:16:35 +0500 Subject: [PATCH 073/106] Updated on 2026-08-14 --- .../v2/stepper/impl/DefaultOnboardingStepperComponent.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt index 03d7c218c2..636c9f4f32 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt @@ -43,8 +43,8 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor( componentScope.launch { val cardInfo = getWalletMetaInfoUseCase(params.scanResponse).getOrNull() ?: return@launch - val userWalletId = cardInfo.userWalletId ?: return@launch - val visaCustomerId = getTangemPayCustomerIdUseCase(userWalletId).getOrNull() + val userWalletId = cardInfo.userWalletId + val visaCustomerId = userWalletId?.let { id -> getTangemPayCustomerIdUseCase(id).getOrNull() } sendFeedbackEmailUseCase( if (params.scanResponse.card.isVisa && !visaCustomerId.isNullOrEmpty()) { FeedbackEmailType.Visa.Activation(walletMetaInfo = cardInfo, customerId = visaCustomerId) From 3c00bf1966a0b87a8e08f5925f117dd5bc1632f6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 13:58:57 +0500 Subject: [PATCH 074/106] Updated on 2026-08-14 --- .../java/com/tangem/data/staking/DefaultStakingRepository.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 626219cb64..bdb663a190 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.staking.store.StakeKitBalancesStore import com.tangem.domain.card.common.TapWorkarounds.isWallet2 import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWallet @@ -156,6 +157,10 @@ internal class DefaultStakingRepository( return false } + if (userWallet.scanResponse.productType == ProductType.Note) { + return true + } + val blockchainId = cryptoCurrency.network.rawId return when { isSolana(blockchainId) -> INVALID_BATCHES_FOR_SOLANA.contains(userWallet.scanResponse.card.batchId) From 1ed2999163acc3917457a884c1510e04645e783d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 13:19:26 +0400 Subject: [PATCH 075/106] Updated on 2026-08-14 --- .../swap/v2/impl/common/entity/ConfirmUM.kt | 6 ++ ...SendWithSwapConfirmSentStateTransformer.kt | 3 + .../success/ui/SendWithSwapSuccessContent.kt | 64 ++++++++++++++++++- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt index cf27f09d02..8be7f8fc02 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt @@ -5,6 +5,9 @@ import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.swap.models.SwapDataModel +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import kotlinx.collections.immutable.ImmutableList @Immutable @@ -37,6 +40,9 @@ internal sealed class ConfirmUM { val txUrl: String, val swapDataModel: SwapDataModel, val provider: ExpressProvider, + val amountUM: SwapAmountUM, + val destinationUM: DestinationUM, + val feeSelectorUM: FeeSelectorUM, ) : ConfirmUM() data object Empty : ConfirmUM() { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmSentStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmSentStateTransformer.kt index 970c3f1f93..d0ade6d256 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmSentStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmSentStateTransformer.kt @@ -20,6 +20,9 @@ internal class SendWithSwapConfirmSentStateTransformer( txUrl = txUrl, provider = provider, swapDataModel = swapDataModel, + amountUM = prevState.amountUM, + destinationUM = prevState.destinationUM, + feeSelectorUM = prevState.feeSelectorUM, ), ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt index 5657f00361..834289a971 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt @@ -93,10 +93,10 @@ internal fun SendWithSwapSuccessContent(sendWithSwapUM: SendWithSwapUM) { @Composable private fun SuccessContent(sendWithSwapUM: SendWithSwapUM, modifier: Modifier = Modifier) { val confirmUM = sendWithSwapUM.confirmUM as? ConfirmUM.Success ?: return - val amountUM = sendWithSwapUM.amountUM as? SwapAmountUM.Content ?: return + val amountUM = confirmUM.amountUM as? SwapAmountUM.Content ?: return val quoteUM = amountUM.selectedQuote as? SwapQuoteUM.Content ?: return - val destinationUM = sendWithSwapUM.destinationUM as? DestinationUM.Content ?: return - val feeSelectorUM = sendWithSwapUM.feeSelectorUM as? FeeSelectorUM.Content ?: return + val destinationUM = confirmUM.destinationUM as? DestinationUM.Content ?: return + val feeSelectorUM = confirmUM.feeSelectorUM as? FeeSelectorUM.Content ?: return Column( modifier = modifier @@ -393,6 +393,64 @@ private fun SendWithSwapSuccessContent_Preview() { txExtraIdName = "Jeffry Blackwell", ), ), + amountUM = SwapAmountContentPreview.defaultState, + destinationUM = DestinationUM.Content( + isPrimaryButtonEnabled = false, + addressTextField = DestinationTextFieldUM.RecipientAddress( + value = "0x391316d97a07027a0702c8A002c8A0C25d8470", + keyboardOptions = KeyboardOptions(), + placeholder = TextReference.EMPTY, + label = resourceReference(R.string.send_recipient), + isError = false, + error = null, + isValuePasted = false, + blockchainAddress = "0x391316d97a07027a0702c8A002c8A0C25d8470", + ), + memoTextField = DestinationTextFieldUM.RecipientMemo( + value = "123123123", + keyboardOptions = KeyboardOptions(), + placeholder = TextReference.EMPTY, + label = resourceReference(R.string.send_recipient), + isError = false, + error = null, + isValuePasted = false, + isEnabled = true, + disabledText = TextReference.EMPTY, + ), + recent = persistentListOf(), + wallets = persistentListOf(), + networkName = "Polygon", + isValidating = false, + isInitialized = false, + isRecentHidden = false, + isAccountsMode = false, + ), + feeSelectorUM = FeeSelectorUM.Content( + fees = TransactionFee.Single( + normal = Fee.Common( + BigDecimal.ONE.convertToSdkAmount( + SwapAmountContentPreview.cryptoCurrencyStatus, + ), + ), + ), + feeItems = persistentListOf(), + selectedFeeItem = FeeItem.Market( + Fee.Common( + BigDecimal.ONE.convertToSdkAmount( + SwapAmountContentPreview.cryptoCurrencyStatus, + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + feeCryptoCurrencyStatus = SwapAmountContentPreview.cryptoCurrencyStatus, + ), + feeFiatRateUM = null, + feeNonce = FeeNonce.None, + isPrimaryButtonEnabled = false, + ), ), navigationUM = NavigationUM.Content( source = SendWithSwapRoute.Success.javaClass.simpleName, From 6e267fc0674cbf69370434fa191074ee804fe41b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 16:49:10 +0400 Subject: [PATCH 076/106] Updated on 2026-08-14 --- .../core/analytics/models/AnalyticsParam.kt | 2 + .../models/event/SwapAnalyticsEvent.kt | 29 +++++++++ .../add/impl/model/AddToPortfolioModel.kt | 2 + .../portfolio/add/impl/model/AddTokenModel.kt | 6 ++ .../impl/analytics/PortfolioAnalyticsEvent.kt | 62 ++++++++++++++++--- .../add/impl/model/AddToPortfolioModel.kt | 2 + .../portfolio/add/impl/model/AddTokenModel.kt | 6 ++ .../impl/analytics/PortfolioAnalyticsEvent.kt | 60 +++++++++++++++--- .../model/AvailableSwapPairsAnalyticsEvent.kt | 44 ------------- .../model/AvailableSwapPairsModel.kt | 38 +++++------- .../feature/swap/analytics/SwapEvents.kt | 19 ------ .../tangem/feature/swap/model/SwapModel.kt | 34 +++++----- 12 files changed, 182 insertions(+), 122 deletions(-) create mode 100644 core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SwapAnalyticsEvent.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt 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 083fa85275..ae0d6a4253 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 @@ -97,6 +97,7 @@ sealed class AnalyticsParam { data object NewsList : ScreensSources("News List") data object NewsLink : ScreensSources("News Link") data object NewsPage : ScreensSources("News Page") + data object Portfolio : ScreensSources("Portfolio") } sealed class TxSentFrom(val value: String) { @@ -292,6 +293,7 @@ sealed class AnalyticsParam { const val ACCOUNT_DERIVATION = "Account Derivation" const val REFERRAL = "Referral" const val REFERRAL_ID = "Referral_ID" + const val SEARCHED = "Searched" } } diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SwapAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SwapAnalyticsEvent.kt new file mode 100644 index 0000000000..e6e5864f91 --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SwapAnalyticsEvent.kt @@ -0,0 +1,29 @@ +package com.tangem.core.analytics.models.event + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.SEARCHED +import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM +import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources + +/** +[REDACTED_AUTHOR] + */ +sealed class SwapAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent("Swap", event, params) { + + data class TokenSelected( + val token: String, + val source: ScreensSources, + val isSearched: Boolean, + ) : SwapAnalyticsEvent( + event = "Token Selected", + params = mapOf( + TOKEN_PARAM to token, + SOURCE to source.value, + SEARCHED to if (isSearched) "True" else "False", + ), + ) +} \ No newline at end of file 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 e234d1d2f6..0d733ab09f 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 @@ -157,6 +157,8 @@ internal class AddToPortfolioModel @Inject constructor( allRequireForAdd.first() // line of navigation to AddToken screen is finished; cancel the job, select a new root screen firstPartOfNavigation.cancel() + + analyticsEventHandler.send(event = eventBuilder.popupToConfirm()) navigation.replaceAll(AddToPortfolioRoutes.AddToken) var middleNavigationJob: Job? = null diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt index cd16df3f04..17d27f1aad 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt @@ -88,6 +88,7 @@ internal class AddTokenModel @Inject constructor( val blockchainNames = listOf(selectedNetwork.selectedNetwork) .mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name } analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames)) + analyticsEventHandler.send(analyticsEventBuilder.addButtonClick()) manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency) .onLeft { throwable -> @@ -110,6 +111,11 @@ internal class AddTokenModel @Inject constructor( } is Account.Payment -> TODO("[REDACTED_JIRA]") } + + analyticsEventHandler.send( + event = analyticsEventBuilder.tokenAdded(status.status.currency.network.name), + ) + params.callbacks.onTokenAdded(status.status) } uiState.value = um.toggleProgress(false) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt index b75d67abf4..87ed9607b7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt @@ -15,28 +15,64 @@ internal class PortfolioAnalyticsEvent( fun addToPortfolioClicked() = PortfolioAnalyticsEvent( event = "Button - Add To Portfolio", - params = mapOf( - "Token" to tokenSymbol, - ), + params = buildMap { + put("Token", tokenSymbol) + if (source != null) put("Source", source) + }, ) fun popupToChooseAccount() = PortfolioAnalyticsEvent( event = "Choose Account Opened", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun popupToConfirm() = PortfolioAnalyticsEvent( + event = "Add Token Screen Opened", + params = buildMap { + if (source != null) put("Source", source) + }, ) fun addToNotMainAccount() = PortfolioAnalyticsEvent( event = "Button - Add To Account", + params = buildMap { + if (source != null) put("Source", source) + }, ) - fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(event = "Wallet Selected") + fun addButtonClick() = PortfolioAnalyticsEvent( + event = "Button - Add Token", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent( + event = "Wallet Selected", + params = buildMap { + if (source != null) put("Source", source) + }, + ) fun addToPortfolioContinue(blockchainNames: List) = PortfolioAnalyticsEvent( event = "Token Network Selected", - params = mapOf( - "Count" to blockchainNames.size.toString(), - "Token" to tokenSymbol, - "blockchain" to blockchainNames.joinToString(separator = ", "), - ), + params = buildMap { + put("Count", blockchainNames.size.toString()) + put("Token", tokenSymbol) + put("blockchain", blockchainNames.joinToString(separator = ", ")) + if (source != null) put("Source", source) + }, + ) + + fun tokenAdded(blockchainName: String) = PortfolioAnalyticsEvent( + event = "Token Added", + params = buildMap { + put("Token", tokenSymbol) + put("Blockchain", blockchainName) + if (source != null) put("Source", source) + }, ) fun quickActionClick(actionUM: TokenActionsBSContentUM.Action, blockchainName: String) = @@ -51,7 +87,7 @@ internal class PortfolioAnalyticsEvent( }, params = buildMap { put("Token", tokenSymbol) - source?.let { put("Source", it) } + if (source != null) put("Source", source) put("blockchain", blockchainName) }, ) @@ -64,10 +100,16 @@ internal class PortfolioAnalyticsEvent( TokenActionsBSContentUM.Action.Stake -> "Popup Get token - Button Stake" else -> "error" }, + params = buildMap { + if (source != null) put("Source", source) + }, ) fun getTokenLater() = PortfolioAnalyticsEvent( event = "Popup Get token - Button Later", + params = buildMap { + if (source != null) put("Source", source) + }, ) } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt index 142c66cf7a..71b3634b3d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -151,6 +151,8 @@ internal class AddToPortfolioModel @Inject constructor( allRequireForAdd.first() // line of navigation to AddToken screen is finished; cancel the job, select a new root screen firstPartOfNavigation.cancel() + + analyticsEventHandler.send(event = eventBuilder.popupToConfirm()) navigation.replaceAll(AddToPortfolioRoutes.AddToken) var middleNavigationJob: Job? = null diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt index edc27eb65f..be45fedb3a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt @@ -88,6 +88,7 @@ internal class AddTokenModel @Inject constructor( val blockchainNames = listOf(selectedNetwork.selectedNetwork) .mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name } analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames)) + analyticsEventHandler.send(analyticsEventBuilder.addButtonClick()) manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency) .onLeft { error -> @@ -110,6 +111,11 @@ internal class AddTokenModel @Inject constructor( } is Account.Payment -> TODO("[REDACTED_JIRA]") } + + analyticsEventHandler.send( + event = analyticsEventBuilder.tokenAdded(status.status.currency.network.name), + ) + params.callbacks.onTokenAdded(status.status) } uiState.value = um.toggleProgress(false) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt index ac6125d3c0..2658bb0659 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt @@ -16,28 +16,64 @@ internal class PortfolioAnalyticsEvent( fun addToPortfolioClicked() = PortfolioAnalyticsEvent( event = "Button - Add To Portfolio", - params = mapOf( - "Token" to token.symbol, - ), + params = buildMap { + put("Token", token.symbol) + if (source != null) put("Source", source) + }, ) fun popupToChooseAccount() = PortfolioAnalyticsEvent( event = "Choose Account Opened", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun popupToConfirm() = PortfolioAnalyticsEvent( + event = "Add Token Screen Opened", + params = buildMap { + if (source != null) put("Source", source) + }, ) fun addToNotMainAccount() = PortfolioAnalyticsEvent( event = "Button - Add To Account", + params = buildMap { + if (source != null) put("Source", source) + }, ) - fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(event = "Wallet Selected") + fun addButtonClick() = PortfolioAnalyticsEvent( + event = "Button - Add Token", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent( + event = "Wallet Selected", + params = buildMap { + if (source != null) put("Source", source) + }, + ) fun addToPortfolioContinue(blockchainNames: List) = PortfolioAnalyticsEvent( event = "Token Network Selected", - params = mapOf( - "Count" to blockchainNames.size.toString(), - "Token" to token.symbol, - "blockchain" to blockchainNames.joinToString(separator = ", "), - ), + params = buildMap { + put("Count", blockchainNames.size.toString()) + put("Token", token.symbol) + put("blockchain", blockchainNames.joinToString(separator = ", ")) + if (source != null) put("Source", source) + }, + ) + + fun tokenAdded(blockchainName: String) = PortfolioAnalyticsEvent( + event = "Token Added", + params = buildMap { + put("Token", token.symbol) + put("Blockchain", blockchainName) + if (source != null) put("Source", source) + }, ) fun quickActionClick(actionUM: TokenActionsBSContentUM.Action, blockchainName: String) = @@ -65,10 +101,16 @@ internal class PortfolioAnalyticsEvent( TokenActionsBSContentUM.Action.Stake -> "Popup Get token - Button Stake" else -> "error" }, + params = buildMap { + if (source != null) put("Source", source) + }, ) fun getTokenLater() = PortfolioAnalyticsEvent( event = "Popup Get token - Button Later", + params = buildMap { + if (source != null) put("Source", source) + }, ) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt deleted file mode 100644 index f5a57df2b5..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.features.onramp.swap.availablepairs.model - -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN -import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM - -private const val SWAP_CATEGORY = "Swap" - -internal sealed class AvailableSwapPairsAnalyticsEvent( - event: String, - params: Map = emptyMap(), -) : AnalyticsEvent(SWAP_CATEGORY, event, params) { - - class TokenSelected( - val token: String, - val source: String, - val isSearched: Boolean, - ) : AvailableSwapPairsAnalyticsEvent( - event = "Token Selected", - params = mapOf( - TOKEN_PARAM to token, - SOURCE to source, - SEARCHED to if (isSearched) "True" else "False", - ), - ) { - companion object { - const val SOURCE = "Source" - const val SEARCHED = "Searched" - const val SOURCE_PORTFOLIO = "Portfolio" - const val SOURCE_MARKETS = "Markets" - } - } - - class TokenAdded( - val token: String, - val blockchain: String, - ) : AvailableSwapPairsAnalyticsEvent( - event = "Token Added", - params = mapOf( - TOKEN_PARAM to token, - BLOCKCHAIN to blockchain, - ), - ) -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index 62822d4108..363f0712d5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -1,11 +1,17 @@ package com.tangem.features.onramp.swap.availablepairs.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.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources +import com.tangem.core.analytics.models.event.SwapAnalyticsEvent import com.tangem.core.decompose.model.Model -import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.fields.InputManager -import com.tangem.core.ui.R as CoreUiR +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.resourceReference @@ -43,9 +49,7 @@ import com.tangem.feature.swap.domain.models.domain.SwapPairLeast 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.onramp.impl.R -import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent -import com.tangem.features.swap.SwapFeatureToggles import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetLoadingTokenItemsTransformer import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformer @@ -58,24 +62,22 @@ import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMController import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer import com.tangem.features.onramp.tokenlist.entity.transformer.* +import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.features.swap.SwapFeatureToggles import com.tangem.lib.crypto.BlockchainUtils -import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate -import com.arkivanov.decompose.router.slot.dismiss import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.runSuspendCatching -import kotlinx.coroutines.ExperimentalCoroutinesApi import com.tangem.utils.coroutines.saveIn +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +import com.tangem.core.ui.R as CoreUiR private typealias AvailablePairsState = Lce> @@ -567,9 +569,9 @@ internal class AvailableSwapPairsModel @Inject constructor( private fun onPortfolioTokenClick(tokenItem: TokenItemState, status: CryptoCurrencyStatus) { analyticsEventHandler.send( - AvailableSwapPairsAnalyticsEvent.TokenSelected( + SwapAnalyticsEvent.TokenSelected( token = status.currency.symbol, - source = AvailableSwapPairsAnalyticsEvent.TokenSelected.SOURCE_PORTFOLIO, + source = ScreensSources.Portfolio, isSearched = state.value.searchBarUM.query.isNotEmpty(), ), ) @@ -686,15 +688,9 @@ internal class AvailableSwapPairsModel @Inject constructor( modelScope.launch { bottomSheetNavigation.dismiss() analyticsEventHandler.send( - AvailableSwapPairsAnalyticsEvent.TokenAdded( + SwapAnalyticsEvent.TokenSelected( token = addedToken.symbol, - blockchain = addedToken.network.name, - ), - ) - analyticsEventHandler.send( - AvailableSwapPairsAnalyticsEvent.TokenSelected( - token = addedToken.symbol, - source = AvailableSwapPairsAnalyticsEvent.TokenSelected.SOURCE_MARKETS, + source = ScreensSources.Markets, isSearched = state.value.searchBarUM.query.isNotEmpty(), ), ) @@ -746,7 +742,7 @@ internal class AvailableSwapPairsModel @Inject constructor( .create( scope = modelScope, token = param, - analyticsParams = null, + analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value), ).apply { setTokenNetworks(networks) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index cd44e2f95c..89a8266fd1 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -46,31 +46,12 @@ sealed class SwapEvents( class ChooseTokenScreenResult( val isTokenChosen: Boolean, val token: String? = null, - val source: String? = null, - val isSearched: Boolean? = null, ) : SwapEvents( event = "Choose Token Screen Result", params = buildMap { put("Token Chosen", if (isTokenChosen) "Yes" else "No") token?.let { put("Token", it) } - source?.let { put(TOKEN_SELECTED_SOURCE, it) } - isSearched?.let { put(SEARCHED, if (it) "True" else "False") } }, - ) { - companion object { - const val TOKEN_SELECTED_SOURCE = "Token Selected Source" - const val SEARCHED = "Searched" - const val SOURCE_PORTFOLIO = "Portfolio" - const val SOURCE_MARKETS = "Markets" - } - } - - class TokenAdded(val token: String, val blockchain: String) : SwapEvents( - event = "Token Added", - params = mapOf( - TOKEN_PARAM to token, - BLOCKCHAIN to blockchain, - ), ) class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents( 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 fb4e74e1ff..0f0e4c1d4c 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 @@ -4,7 +4,6 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue - import arrow.core.Either import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation @@ -19,12 +18,15 @@ 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 +import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.models.event.SwapAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles +import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.InputNumberFormatter @@ -90,7 +92,6 @@ 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.core.ui.R import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder @@ -107,12 +108,8 @@ 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 kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode @@ -300,17 +297,13 @@ internal class SwapModel @Inject constructor( modelScope.launch { bottomSheetNavigation.dismiss() analyticsEventHandler.send( - SwapEvents.ChooseTokenScreenResult( - isTokenChosen = true, - token = addedToken.symbol, - source = SwapEvents.ChooseTokenScreenResult.SOURCE_MARKETS, - isSearched = searchQueryState.value.isNotEmpty(), - ), + SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = addedToken.symbol), ) analyticsEventHandler.send( - SwapEvents.TokenAdded( + SwapAnalyticsEvent.TokenSelected( token = addedToken.symbol, - blockchain = addedToken.network.name, + source = ScreensSources.Markets, + isSearched = searchQueryState.value.isNotEmpty(), ), ) searchQueryState.value = "" @@ -1339,10 +1332,13 @@ internal class SwapModel @Inject constructor( foundToken?.currency?.symbol?.let { symbol -> analyticsEventHandler.send( - SwapEvents.ChooseTokenScreenResult( - isTokenChosen = true, + SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = symbol), + ) + + analyticsEventHandler.send( + SwapAnalyticsEvent.TokenSelected( token = symbol, - source = SwapEvents.ChooseTokenScreenResult.SOURCE_PORTFOLIO, + source = ScreensSources.Portfolio, isSearched = searchQueryState.value.isNotEmpty(), ), ) @@ -2241,7 +2237,7 @@ internal class SwapModel @Inject constructor( .create( scope = modelScope, token = param, - analyticsParams = null, + analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value), ).apply { setTokenNetworks(networks) } From 8f4a8b18ef9f6c3a91f6e2600a43a45812b1b98c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 09:59:10 +0400 Subject: [PATCH 077/106] Updated on 2026-08-14 --- .../api/express/models/response/SwapPair.kt | 6 +++++- .../tangem/data/swap/DefaultSwapRepositoryV2.kt | 14 ++++++++------ .../swap/converters/SwapPairInfoConverter.kt | 6 +++--- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt index 9b96730278..21f255653b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt @@ -24,7 +24,11 @@ data class SwapPairProvider( @Json(name = "rateTypes") val rateTypes: List, -) +) { + fun hasOnlyFixedRateType(): Boolean { + return rateTypes.isNotEmpty() && rateTypes.all { it == RateType.FIXED } + } +} @JsonClass(generateAdapter = false) enum class RateType { 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 b229533df3..080f06bacf 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 @@ -94,9 +94,10 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( currencyStatus.currency.network.backendId == pair.to.network } - val mappedProviders = pair.providers.mapNotNull { - expressProviders[it.providerId] - }.filterYieldSupplyProvider(statusFrom) + val mappedProviders = pair.providers + .filterNot { it.hasOnlyFixedRateType() } + .mapNotNull { expressProviders[it.providerId] } + .filterYieldSupplyProvider(statusFrom) if (statusFrom != null && statusTo != null && mappedProviders.isNotEmpty()) { SwapPairModel( @@ -151,9 +152,10 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( val currencyStatusFrom = createSendWithSwapCryptoCurrencyStatus(statusFromDeferred.await()) val currencyStatusTo = createSendWithSwapCryptoCurrencyStatus(statusToDeferred.await()) - val mappedProvider = pair.providers.mapNotNull { - mappedProviders[it.providerId] - }.filterYieldSupplyProvider(currencyStatusFrom) + val mappedProvider = pair.providers + .filterNot { it.hasOnlyFixedRateType() } + .mapNotNull { mappedProviders[it.providerId] } + .filterYieldSupplyProvider(currencyStatusFrom) if (currencyStatusFrom != null && currencyStatusTo != null && mappedProvider.isNotEmpty()) { SwapPairModel( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index 1080578a05..414a2649d7 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -25,9 +25,9 @@ class SwapPairInfoConverter : Converter Date: Tue, 24 Feb 2026 13:38:12 +0400 Subject: [PATCH 078/106] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 57 ++++-- .../local/preferences/PreferencesKeys.kt | 5 +- .../datasource/local/visa/TangemPayStorage.kt | 18 +- .../DefaultTangemPayWithdrawRepository.kt | 178 +++++++----------- .../repository/TangemPayWithdrawRepository.kt | 3 +- .../tangempay/model/TangemPayDetailsModel.kt | 2 +- .../state/factory/ExpressStateFactory.kt | 6 +- 7 files changed, 136 insertions(+), 133 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 5708a79d8b..42450edff8 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -1,7 +1,9 @@ package com.tangem.tap.data import android.content.Context +import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi +import com.squareup.moshi.Types import com.tangem.data.pay.entity.WithdrawStoreData import com.tangem.data.pay.util.WithdrawStateConverter import com.tangem.data.pay.util.WithdrawStoreDataConverter @@ -19,6 +21,7 @@ import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.withContext import java.util.UUID import javax.inject.Inject @@ -137,28 +140,56 @@ internal class DefaultTangemPayStorage @Inject constructor( return appPreferencesStore.getSyncOrNull(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId)) } - override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) { + override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) { appPreferencesStore.editData { mutablePreferences -> - val orders = mutablePreferences.getObjectMap( - PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, - ) - .plus(createWithdrawOrderIdKey(userWalletId) to withdrawStoreDataConverter.convert(data)) + val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + .plus(createWithdrawOrderIdKey(userWalletId) to orderId) mutablePreferences.setObjectMap( - key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, + key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, value = orders, ) } } - override suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState? { - val orders = appPreferencesStore.getObjectMapSync( - PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, - ) - val data = orders[createWithdrawOrderIdKey(userWalletId)] ?: return null - return withdrawStateConverter.convert(data) + override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) { + val listType = Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java) + val mapType = Types.newParameterizedType(Map::class.java, String::class.java, listType) + val adapter: JsonAdapter>> = appPreferencesStore.moshi.adapter(mapType) + appPreferencesStore.editData { prefs -> + val walletKey = createWithdrawOrderIdKey(userWalletId) + val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]?.let(adapter::fromJson).orEmpty() + val updatedList = currentMap[walletKey].orEmpty() + withdrawStoreDataConverter.convert(data) + prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = adapter + .toJson(currentMap + (walletKey to updatedList)) + } } - override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) { + override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? { + val orders = appPreferencesStore.getObjectMapSync(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + return orders[createWithdrawOrderIdKey(userWalletId)] + } + + override suspend fun getWithdrawOrders(userWalletId: UserWalletId): List { + val listType = Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java) + val mapType = Types.newParameterizedType(Map::class.java, String::class.java, listType) + val adapter: JsonAdapter>> = appPreferencesStore.moshi.adapter(mapType) + val map = appPreferencesStore.data.firstOrNull() + ?.get(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)?.let(adapter::fromJson).orEmpty() + return map[createWithdrawOrderIdKey(userWalletId)].orEmpty().map(withdrawStateConverter::convert) + } + + override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) { + appPreferencesStore.editData { mutablePreferences -> + val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + .minus(createWithdrawOrderIdKey(userWalletId)) + mutablePreferences.setObjectMap( + key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, + value = orders, + ) + } + } + + override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) { appPreferencesStore.editData { mutablePreferences -> val orders = mutablePreferences.getObjectMap( PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, 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 e0c785a2e8..c70f061ee1 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 @@ -161,7 +161,10 @@ object PreferencesKeys { intPreferencesKey(name = "tronNetworkFeeNotificationShowCount") } - val TANGEM_PAY_WITHDRAW_ORDERS_KEY by lazy { stringPreferencesKey(name = "tangemPayWithdrawOrders") } + val TANGEM_PAY_WITHDRAW_ORDERS_KEY by lazy { stringPreferencesKey(name = "tangemPayWithdrawOrdersKey") } + val TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY by lazy { + stringPreferencesKey(name = "tangemPayActiveWithdrawOrdersKey") + } val TANGEM_PAY_ELIGIBILITY_KEY by lazy { booleanPreferencesKey(name = "tangemPayEligibility") } fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key") 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 bd29637e98..25a33b8970 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 @@ -29,11 +29,25 @@ interface TangemPayStorage { suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? + /** Called after creating withdraw order, active order id */ + suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) + + /** Called after creating withdraw order, saves order data */ suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) - suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState? + /** Returns single active order id. Once the order is completed, deletes id from storage. + * Only one active order allowed for a wallet */ + suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? - suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) + /** Returns all withdraw orders saved. + * Once we get tx hash for an order, it gets deleted from this storage */ + suspend fun getWithdrawOrders(userWalletId: UserWalletId): List? + + /** Deletes active withdraw order. Called after order is completed */ + suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) + + /** Deletes withdraw order data. Called after getting its tx hash */ + suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean 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 cea8e61d60..922b617842 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 @@ -2,7 +2,6 @@ package com.tangem.data.pay.repository import arrow.core.Either import arrow.core.left -import arrow.core.right import com.tangem.core.error.UniversalError import com.tangem.data.common.quote.QuotesFetcher import com.tangem.datasource.api.pay.TangemPayApi @@ -17,7 +16,6 @@ import com.tangem.domain.pay.TangemPayWithdrawState import com.tangem.domain.pay.WithdrawalResult import com.tangem.domain.pay.WithdrawalSignatureResult import com.tangem.domain.pay.datasource.TangemPayAuthDataSource -import com.tangem.domain.pay.model.OrderData import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.TangemPayWithdrawRepository @@ -25,13 +23,7 @@ import com.tangem.domain.visa.error.VisaApiError import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.utils.extensions.addHexPrefix -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber @@ -40,7 +32,6 @@ import java.math.RoundingMode import java.util.Currency import java.util.Locale import javax.inject.Inject -import kotlin.collections.set import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration.Companion.seconds @@ -113,23 +104,18 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( ) { val orderId = response.result?.orderId if (orderId != null) { - val orderData = orderRepository - .getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() - val withdrawTxHash = orderData?.withdrawTxHash + tangemPayStorage.storeActiveWithdrawOrderId(userWalletId = userWallet.walletId, orderId = orderId) + val order = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + val withdrawTxHash = order?.withdrawTxHash val storeData = TangemPayWithdrawState( orderId = orderId, exchangeData = exchangeData, ) - if (orderData != null && !withdrawTxHash.isNullOrEmpty()) { - finalizeWithdraw( - userWallet = userWallet, - withdrawTxHash = withdrawTxHash, - orderId = orderId, - exchangeData = exchangeData, - order = orderData, - ).onLeft { - tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) - } + if (order != null && !withdrawTxHash.isNullOrEmpty()) { + finalizeWithdraw(userWallet = userWallet, txHash = withdrawTxHash, exchangeData = exchangeData) + .onLeft { + tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) + } } else { tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) } @@ -138,10 +124,8 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( private suspend fun finalizeWithdraw( userWallet: UserWallet, - withdrawTxHash: String, - orderId: String, + txHash: String, exchangeData: TangemPayWithdrawExchangeState, - order: OrderData, ): Either { return swapRepository.exchangeSent( userWallet = userWallet, @@ -149,89 +133,63 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( fromNetwork = exchangeData.fromNetwork, fromAddress = exchangeData.fromAddress, payInAddress = exchangeData.payInAddress, - txHash = withdrawTxHash, + txHash = txHash, payInExtraId = exchangeData.payInExtraId, ) - .onRight { - val isActive = order.status == OrderStatus.NEW || order.status == OrderStatus.PROCESSING - if (!isActive) { - tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId) - } else { - tangemPayStorage.storeWithdrawOrder( - userWalletId = userWallet.walletId, - data = TangemPayWithdrawState(orderId = orderId, exchangeData = null), - ) - } - } .onLeft { error -> Timber.tag(TAG).e(error.toString()) } } override suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean { - val orderExchangeData = tangemPayStorage.getWithdrawOrder(userWallet.walletId) - if (orderExchangeData == null) return false - - val exchangeData = orderExchangeData.exchangeData - val orderData = orderRepository - .getOrderData(userWallet.walletId, orderId = orderExchangeData.orderId).getOrNull() - val withdrawTxHash = orderData?.withdrawTxHash - - if (exchangeData != null && orderData != null && withdrawTxHash != null) { - finalizeWithdraw( - userWallet = userWallet, - withdrawTxHash = withdrawTxHash, - orderId = orderExchangeData.orderId, - exchangeData = exchangeData, - order = orderData, - ) + val orderId = tangemPayStorage.getActiveWithdrawOrderId(userWallet.walletId) + if (orderId.isNullOrEmpty()) return false + val orderData = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + val isActive = orderData?.status == OrderStatus.NEW || orderData?.status == OrderStatus.PROCESSING + if (!isActive) { + tangemPayStorage.deleteActiveWithdrawOrder(userWalletId = userWallet.walletId) } - - return orderData?.status == OrderStatus.NEW || orderData?.status == OrderStatus.PROCESSING + return isActive } - override suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either { - val storeData = tangemPayStorage.getWithdrawOrder(userWallet.walletId) ?: return Unit.right() - val exchangeData = storeData.exchangeData ?: return Unit.right() - - val orderId = storeData.orderId - val order = orderRepository - .getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() - ?: return Unit.right() + override suspend fun pollWithdrawOrdersIfNeeds(userWallet: UserWallet) { + tangemPayStorage.getWithdrawOrders(userWalletId = userWallet.walletId)?.forEach { state -> + withdrawPollingScope.launch { + try { + pollWithdrawOrderIfNeeds(userWallet = userWallet, data = state) + } catch (exception: CancellationException) { + throw exception + } catch (exception: Exception) { + Timber.tag(TAG).e(exception) + } + } + } + } + private suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet, data: TangemPayWithdrawState) { + val exchangeData = data.exchangeData ?: return + val orderId = data.orderId + val order = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + ?: return val txHash = order.withdrawTxHash if (!txHash.isNullOrEmpty()) { - finalizeWithdraw( - userWallet = userWallet, - withdrawTxHash = txHash, - orderId = storeData.orderId, - exchangeData = exchangeData, - order = order, - ).onLeft { - startWithdrawOrderPolling( - userWallet = userWallet, - orderId = orderId, - storeData = storeData, - exchangeData = exchangeData, - ) - } + finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData) + .onRight { + tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) + } + .onLeft { + startWithdrawOrderPolling(userWallet = userWallet, orderId = orderId, exchangeData = exchangeData) + } } else { - startWithdrawOrderPolling( - userWallet = userWallet, - orderId = orderId, - storeData = storeData, - exchangeData = exchangeData, - ) + startWithdrawOrderPolling(userWallet = userWallet, orderId = orderId, exchangeData = exchangeData) } - - return Unit.right() + return } private suspend fun startWithdrawOrderPolling( userWallet: UserWallet, orderId: String, - storeData: TangemPayWithdrawState, exchangeData: TangemPayWithdrawExchangeState, ) { withdrawPollingMutex.withLock { @@ -239,33 +197,33 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( val pollingJob = withdrawPollingScope.launch { try { - while (isActive && withdrawPollingJobs.containsKey(orderId)) { + while (isActive) { delay(duration = 5.seconds) - val orderData = orderRepository - .getOrderData(userWalletId = userWallet.walletId, orderId = orderId) - orderData.onRight { order -> - if (order.status != OrderStatus.NEW && order.status != OrderStatus.PROCESSING) { - tangemPayStorage.deleteWithdrawOrder(userWallet.walletId) - withdrawPollingJobs.remove(key = orderId) + orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId) + .onRight { order -> + val txHash = order.withdrawTxHash + if (txHash.isNullOrEmpty()) return@onRight + finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData) + .onRight { + tangemPayStorage.deleteWithdrawOrder( + userWalletId = userWallet.walletId, + orderId = orderId, + ) + withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } + return@launch + } + .onLeft { error -> + Timber.tag(TAG).e("finalizeWithdraw error: $error") + withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } + return@launch + } + } + .onLeft { error -> + Timber.tag(TAG).e("getOrderData error ${error.errorCode}") + withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } return@launch } - val txHash = order.withdrawTxHash - if (!txHash.isNullOrEmpty()) { - finalizeWithdraw( - userWallet = userWallet, - withdrawTxHash = txHash, - orderId = storeData.orderId, - exchangeData = exchangeData, - order = order, - ).onRight { - withdrawPollingJobs.remove(key = orderId) - return@launch - } - } - }.onLeft { error -> - Timber.tag(TAG).e("error ${error.errorCode}") - } } } catch (exception: CancellationException) { throw exception diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt index 18b09d331e..6079a4e874 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt @@ -6,7 +6,6 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayWithdrawExchangeState import com.tangem.domain.pay.WithdrawalResult -import com.tangem.domain.visa.error.VisaApiError import java.math.BigDecimal interface TangemPayWithdrawRepository { @@ -21,5 +20,5 @@ interface TangemPayWithdrawRepository { suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean - suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either + suspend fun pollWithdrawOrdersIfNeeds(userWallet: UserWallet) } \ No newline at end of file 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 793549861d..beb2bed482 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 @@ -153,7 +153,7 @@ internal class TangemPayDetailsModel @Inject constructor( modelScope.launch { val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull() ?: return@launch - tangemPayWithdrawRepository.pollWithdrawOrderIfNeeds(userWallet) + tangemPayWithdrawRepository.pollWithdrawOrdersIfNeeds(userWallet) } } 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 e888745a6f..950bd50f41 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 @@ -25,14 +25,12 @@ internal class ExpressStateFactory( fun getStateWithClosedDialog(): ExpressTransactionsBlockState { val state = currentStateProvider() - val slot = state.dialogSlot ?: return state - return state.copy(dialogSlot = slot.copy(config = slot.config.copy(isShow = false))) + return state.copy(dialogSlot = null) } fun getStateWithClosedBottomSheet(): ExpressTransactionsBlockState { val state = currentStateProvider() - val slot = state.bottomSheetSlot ?: return state - return state.copy(bottomSheetSlot = slot.copy(config = slot.config.copy(isShown = false))) + return state.copy(bottomSheetSlot = null) } fun getStateWithConfirmHideExpressStatus(): ExpressTransactionsBlockState { From 80c1b5ba20540cb8bbc3a9fa4268d0e3f567357c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 18:24:23 +0400 Subject: [PATCH 079/106] Updated on 2026-08-14 --- .../tangem/feature/swap/model/SwapModel.kt | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) 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 0f0e4c1d4c..4f2656df25 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 @@ -510,14 +510,7 @@ internal class SwapModel @Inject constructor( dataState.fromCryptoCurrency } - fromCryptoCurrency?.let { cryptoCurrency -> - dataState = dataState.copy( - feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = userWalletId, - cryptoCurrencyStatus = cryptoCurrency, - ).getOrNull(), - ) - } + if (fromCryptoCurrency != null) updateFeePaidCryptoCurrencyFor(fromCryptoCurrency) subscribeToCoinBalanceUpdatesIfNeeded() }.onFailure { error -> @@ -754,6 +747,15 @@ internal class SwapModel @Inject constructor( } } + private suspend fun updateFeePaidCryptoCurrencyFor(fromToken: CryptoCurrencyStatus) { + dataState = dataState.copy( + feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = fromToken, + ).getOrNull(), + ) + } + private fun loadQuotesTask( fromToken: CryptoCurrencyStatus, fromAccount: Account.CryptoPortfolio?, @@ -1402,6 +1404,9 @@ internal class SwapModel @Inject constructor( toAccount = toAccount, selectedProvider = null, ) + modelScope.launch { + updateFeePaidCryptoCurrencyFor(fromToken) + } startLoadingQuotes( fromToken = fromToken, fromAccount = fromAccount, @@ -1544,6 +1549,7 @@ internal class SwapModel @Inject constructor( toAccount = newToAccount, ) isOrderReversed = !isOrderReversed + updateFeePaidCryptoCurrencyFor(newFromToken) dataState.tokensDataState?.let { tokensDataState -> updateTokensState(tokensDataState) } From 63698bed23cfe8348ad162da9af708109bdb01a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 17:35:17 +0300 Subject: [PATCH 080/106] Updated on 2026-08-14 --- .../com/tangem/feature/swap/ui/StateBuilder.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 6c955e7c86..adeed21292 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 @@ -170,10 +170,10 @@ internal class StateBuilder( amountTextFieldValue = null, amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", token = fromToken, - tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, - coinId = uiStateHolder.sendCardData.coinId, - isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, - tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, + tokenIconUrl = fromToken.currency.iconUrl, + coinId = fromToken.currency.network.backendId, + isNotNativeToken = fromToken.currency is CryptoCurrency.Token, + tokenCurrency = fromToken.currency.symbol, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, balance = fromToken.getFormattedAmount(isNeedSymbol = false), networkIconRes = getActiveIconRes(fromToken.currency.network.rawId), @@ -213,10 +213,10 @@ internal class StateBuilder( amountTextFieldValue = null, amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", token = fromToken, - tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, - coinId = uiStateHolder.sendCardData.coinId, - isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, - tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, + tokenIconUrl = fromToken.currency.iconUrl, + coinId = fromToken.currency.network.backendId, + isNotNativeToken = fromToken.currency is CryptoCurrency.Token, + tokenCurrency = fromToken.currency.symbol, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, balance = fromToken.getFormattedAmount(isNeedSymbol = false), networkIconRes = getActiveIconRes(fromToken.currency.network.rawId), From 189cb19ff2fdcb99d7803d75995d0d5d0594171e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 19:49:20 +0400 Subject: [PATCH 081/106] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 6 +- domain/wallets/build.gradle.kts | 1 + .../usecase/SetNotificationsEnabledUseCase.kt | 6 +- .../SetNotificationsEnabledUseCaseTest.kt | 131 ++++++++++++++++++ 4 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCaseTest.kt 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 fa95151e3b..5e55ebdd54 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 @@ -1,9 +1,9 @@ package com.tangem.tap.di.domain import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase @@ -272,11 +272,11 @@ internal object WalletsDomainModule { @Singleton fun providesSetNotificationsEnabledUseCase( walletsRepository: WalletsRepository, - currenciesRepository: CurrenciesRepository, + accountsCRUDRepository: AccountsCRUDRepository, ): SetNotificationsEnabledUseCase { return SetNotificationsEnabledUseCase( walletsRepository = walletsRepository, - currenciesRepository = currenciesRepository, + accountsCRUDRepository = accountsCRUDRepository, ) } diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index b7b3798d96..f11102091a 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(projects.domain.walletManager) implementation(projects.libs.blockchainSdk) implementation(projects.libs.tangemSdkApi) + implementation(projects.domain.account) implementation(projects.domain.models) implementation(projects.domain.tokens) implementation(projects.domain.card) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt index 5a66ceb5ab..c749d56049 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt @@ -1,13 +1,13 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either -import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.repository.WalletsRepository class SetNotificationsEnabledUseCase( private val walletsRepository: WalletsRepository, - private val currenciesRepository: CurrenciesRepository, + private val accountsCRUDRepository: AccountsCRUDRepository, ) { suspend operator fun invoke(userWalletId: UserWalletId, isEnabled: Boolean): Either = @@ -16,7 +16,7 @@ class SetNotificationsEnabledUseCase( userWalletId = userWalletId, isEnabled = isEnabled, ) - currenciesRepository.syncTokens(userWalletId) + accountsCRUDRepository.syncTokens(userWalletId) }.onLeft { walletsRepository.setNotificationsEnabled( userWalletId = userWalletId, diff --git a/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCaseTest.kt b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCaseTest.kt new file mode 100644 index 0000000000..73d291ba22 --- /dev/null +++ b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCaseTest.kt @@ -0,0 +1,131 @@ +package com.tangem.domain.wallets.usecase + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifyOrder +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class SetNotificationsEnabledUseCaseTest { + + private lateinit var useCase: SetNotificationsEnabledUseCase + private lateinit var walletsRepository: WalletsRepository + private lateinit var accountsCRUDRepository: AccountsCRUDRepository + + @Before + fun setup() { + walletsRepository = mockk() + accountsCRUDRepository = mockk() + useCase = SetNotificationsEnabledUseCase( + walletsRepository = walletsRepository, + accountsCRUDRepository = accountsCRUDRepository, + ) + } + + @Test + fun `GIVEN notifications enabled successfully WHEN invoke THEN return Right with Unit`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = true + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } just runs + coEvery { accountsCRUDRepository.syncTokens(userWalletId) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isRight()).isTrue() + coVerifyOrder { + walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) + accountsCRUDRepository.syncTokens(userWalletId) + } + } + + @Test + fun `GIVEN notifications disabled successfully WHEN invoke THEN return Right with Unit`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = false + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } just runs + coEvery { accountsCRUDRepository.syncTokens(userWalletId) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isRight()).isTrue() + coVerifyOrder { + walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) + accountsCRUDRepository.syncTokens(userWalletId) + } + } + + @Test + fun `GIVEN setNotificationsEnabled throws exception WHEN invoke THEN return Left and revert notifications`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = true + val exception = RuntimeException("Network error") + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } throws exception + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isLeft()).isTrue() + result.onLeft { throwable -> + assertThat(throwable).isEqualTo(exception) + } + coVerify { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } + } + + @Test + fun `GIVEN syncTokens throws exception WHEN invoke THEN return Left and revert notifications`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = true + val exception = RuntimeException("Sync error") + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } just runs + coEvery { accountsCRUDRepository.syncTokens(userWalletId) } throws exception + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isLeft()).isTrue() + result.onLeft { throwable -> + assertThat(throwable).isEqualTo(exception) + } + coVerify { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } + } + + @Test + fun `GIVEN disabling notifications fails WHEN invoke THEN return Left and revert to enabled`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = false + val exception = RuntimeException("Network error") + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } throws exception + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isLeft()).isTrue() + result.onLeft { throwable -> + assertThat(throwable).isEqualTo(exception) + } + coVerify { walletsRepository.setNotificationsEnabled(userWalletId, true) } + } +} \ No newline at end of file From 55b6b5796375b3e2aeb8c15e9e3a977f971f230b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 19:37:46 +0300 Subject: [PATCH 082/106] Updated on 2026-08-14 --- .../swap/component/SwapFeeSelectorBlockComponent.kt | 10 +++++++++- .../java/com/tangem/feature/swap/model/SwapModel.kt | 13 +++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/component/SwapFeeSelectorBlockComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/component/SwapFeeSelectorBlockComponent.kt index 19e1760238..96a5d81ee3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/component/SwapFeeSelectorBlockComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/component/SwapFeeSelectorBlockComponent.kt @@ -20,6 +20,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -55,7 +56,11 @@ class SwapFeeSelectorBlockComponent @AssistedInject constructor( init { params.repository.state - .onEach(feeSelectorBlockComponent::updateState) + .onEach { feeSelectorBlockComponent.updateState(it) } + .launchIn(componentScope) + + params.repository.forceUpdateState + .onEach { feeSelectorBlockComponent.updateState(it) } .launchIn(componentScope) } @@ -68,6 +73,9 @@ class SwapFeeSelectorBlockComponent @AssistedInject constructor( val state: StateFlow get() = MutableStateFlow(FeeSelectorUM.Loading) + val forceUpdateState: SharedFlow + get() = MutableStateFlow(FeeSelectorUM.Loading) + fun onResult(newState: FeeSelectorUM) suspend fun loadFee(): Either 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 4f2656df25..38be62641a 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 @@ -2325,6 +2325,8 @@ internal class SwapModel @Inject constructor( FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true), ) + override val forceUpdateState = MutableSharedFlow() + override suspend fun loadFeeExtended( selectedToken: CryptoCurrencyStatus?, ): Either { @@ -2357,18 +2359,13 @@ internal class SwapModel @Inject constructor( } override fun onResult(newState: FeeSelectorUM) { - if (isPermissionNotificationShown()) { - state.value = FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true) - return - } - if (newState is FeeSelectorUM.Error) { - state.value = newState.copy(isHidden = true) + modelScope.launch { + forceUpdateState.emit(newState.copy(isHidden = true)) + } return } - state.value = newState - // If fee currency is same as from currency, we need to reload quotes to update fee info val isFeeCurrencySameAsFromCurrency = newState is FeeSelectorUM.Content && dataState.fromCryptoCurrency?.currency?.id == newState.feeExtraInfo.feeCryptoCurrencyStatus.currency.id From 60e697282b5e9b1bb923b9ce03388578dfe322ed Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 21:40:36 +0500 Subject: [PATCH 083/106] Updated on 2026-08-14 --- .../usecase/ManageCryptoCurrenciesUseCase.kt | 15 +++++++++++++-- .../referral/domain/ReferralInteractorImpl.kt | 12 +++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) 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 d41defe9be..2bef64e9b1 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 @@ -59,14 +59,21 @@ class ManageCryptoCurrenciesUseCase( accountId: AccountId, add: CryptoCurrency? = null, remove: CryptoCurrency? = null, + skipDerivationErrors: Boolean = true, ): Either { - return invoke(accountId = accountId, add = listOfNotNull(add), remove = listOfNotNull(remove)) + return invoke( + accountId = accountId, + add = listOfNotNull(add), + remove = listOfNotNull(remove), + skipDerivationErrors = skipDerivationErrors, + ) } suspend operator fun invoke( accountId: AccountId, add: List = emptyList(), remove: List = emptyList(), + skipDerivationErrors: Boolean = true, ): Either = eitherOn(dispatchers.default) { if (add.isEmpty() && remove.isEmpty()) { Timber.d("No currencies to add or remove, skipping") @@ -89,7 +96,7 @@ class ManageCryptoCurrenciesUseCase( account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total), ) - derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) + val result = derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) parallelUpdatingScope.launch { syncTokens(userWalletId, modifiedCurrencyList) @@ -98,6 +105,10 @@ class ManageCryptoCurrenciesUseCase( refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total) clearMetadata(userWalletId = userWalletId, currencies = modifiedCurrencyList.removed) } + + if (!skipDerivationErrors) { + result.bind() + } } } 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 cfbdeea741..930418c920 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 @@ -73,7 +73,17 @@ internal class ReferralInteractorImpl( when (portfolioId) { is PortfolioId.Account -> { - manageCryptoCurrenciesUseCase(accountId = portfolioId.accountId, add = cryptoCurrency) + manageCryptoCurrenciesUseCase( + accountId = portfolioId.accountId, + add = cryptoCurrency, + skipDerivationErrors = false, + ).mapLeft { + it.mapToDomainError() + }.onLeft { error -> + if (error is ReferralError.UserCancelledException) { + throw error + } + } } is PortfolioId.Wallet -> { derivePublicKeysUseCase(userWallet.walletId, listOf(cryptoCurrency)).getOrElse { throwable -> From caa14291752daa6c99938780ebf958d07d852b70 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 23:46:56 +0700 Subject: [PATCH 084/106] Updated on 2026-08-14 --- .../domain/scanCard/LegacyScanProcessor.kt | 99 +++++-------------- .../sdk/impl/DefaultTangemSdkManager.kt | 1 + .../tasks/product/ResetBackupCardTask.kt | 1 + .../domain/tasks/product/ScanProductTask.kt | 10 ++ .../tap/domain/twins/FinalizeTwinTask.kt | 6 +- gradle/tangem_dependencies.toml | 2 +- 6 files changed, 41 insertions(+), 78 deletions(-) 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 95d2b8fe04..ad63237780 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,15 +19,15 @@ 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.common.extensions.withMainContext import com.tangem.domain.card.common.util.twinsIsTwinned +import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.sdk.extensions.localizedDescriptionRes import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor -import com.tangem.tap.common.extensions.* -import com.tangem.tap.common.redux.AppDialog +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 @@ -113,7 +113,6 @@ internal class LegacyScanProcessor @Inject constructor( onProgressStateChange = onProgressStateChange, onSuccess = onSuccess, onWalletNotCreated = onWalletNotCreated, - onCancel = onCancel, ) }, ) @@ -198,90 +197,38 @@ internal class LegacyScanProcessor @Inject constructor( scanResponse: ScanResponse, crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit, crossinline onWalletNotCreated: suspend () -> Unit, - crossinline onCancel: suspend () -> Unit, crossinline onSuccess: suspend (ScanResponse) -> Unit, ) { - checkCardWasUsedInApp( - scanResponse = scanResponse, - onCancel = { - mainScope.launch { - onProgressStateChange.invoke(false) - onCancel() - } - }, - ) { - if (OnboardingHelper.isOnboardingCase(scanResponse)) { - trackingContextProxy.addContext(scanResponse) + if (OnboardingHelper.isOnboardingCase(scanResponse)) { + trackingContextProxy.addContext(scanResponse) + onWalletNotCreated() + navigateTo( + AppRoute.Onboarding( + scanResponse = scanResponse, + mode = AppRoute.Onboarding.Mode.Onboarding, + ), + ) { onProgressStateChange(it) } + } else { + trackingContextProxy.setContext(scanResponse) + + val wasTwinsOnboardingShown = + store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync() + + if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) { onWalletNotCreated() navigateTo( AppRoute.Onboarding( scanResponse = scanResponse, - mode = AppRoute.Onboarding.Mode.Onboarding, + mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, ), ) { onProgressStateChange(it) } } else { - trackingContextProxy.setContext(scanResponse) - - val wasTwinsOnboardingShown = - store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync() - - if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) { - onWalletNotCreated() - navigateTo( - AppRoute.Onboarding( - scanResponse = scanResponse, - mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, - ), - ) { onProgressStateChange(it) } - } else { - delay(DELAY_SDK_DIALOG_CLOSE) - onSuccess(scanResponse) - } + delay(DELAY_SDK_DIALOG_CLOSE) + onSuccess(scanResponse) } } } - /** - * Checks if card has password and never login at this app - * Show alert in this case - */ - private suspend fun checkCardWasUsedInApp( - scanResponse: ScanResponse, - onCancel: () -> Unit, - onSuccess: suspend () -> Unit, - ) { - val userWalletId = runCatching { UserWalletIdBuilder.card(scanResponse.card).build() }.getOrNull() - if (userWalletId == null) { - onSuccess() - return - } - - val userTokensResponseStore = store.inject(DaggerGraphState::userTokensResponseStore) - val tokens = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - - if (scanResponse.card.isAccessCodeSet && tokens == null) { - store.dispatchDialogShow( - AppDialog.WalletAlreadyWasUsedDialog( - onOk = { mainScope.launch { onSuccess() } }, - onSupportClick = { - val cardInfo = - store.inject(DaggerGraphState::getWalletMetaInfoUseCase).invoke(scanResponse).getOrNull() - ?: error("CardInfo must be not null") - - scope.launch { - store.inject(DaggerGraphState::sendFeedbackEmailUseCase) - .invoke(type = FeedbackEmailType.PreActivatedWallet(cardInfo)) - } - onCancel() - }, - onCancel = { onCancel() }, - ), - ) - } else { - onSuccess() - } - } - private suspend inline fun navigateTo(route: AppRoute, onProgressStateChange: (showProgress: Boolean) -> Unit) { delay(DELAY_SDK_DIALOG_CLOSE) store.dispatchNavigationAction { push(route) } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 11a3ffb59b..a9c061c5f7 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -176,6 +176,7 @@ internal class DefaultTangemSdkManager( allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, visaCardScanHandler = visaCardScanHandler, visaCoroutineScope = this, + shouldCheckIsAlreadyActivated = true, onboardingV2FeatureToggles = onboardingV2FeatureToggles, ), cardId = cardId, diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt index bb0fafa039..f1a1683517 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt @@ -27,6 +27,7 @@ internal class ResetBackupCardTask( PreflightReadTask( readMode = PreflightReadMode.FullCardRead, filter = UserWalletIdPreflightReadFilter(expectedUserWalletId = userWalletId), + secureStorage = session.environment.secureStorage, ).run(session) { result -> when (result) { is CompletionResult.Success -> resetCard(session, callback) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 93a1c1f2be..4c334fd027 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -29,6 +29,7 @@ import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles +import com.tangem.operations.PreflightReadMode import com.tangem.operations.ScanTask import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.backup.StartPrimaryCardLinkingTask @@ -52,6 +53,7 @@ internal class ScanProductTask( private val visaCardScanHandler: VisaCardScanHandler?, private val visaCoroutineScope: CoroutineScope?, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?, + private val shouldCheckIsAlreadyActivated: Boolean, override val allowsRequestAccessCodeFromRepository: Boolean = false, ) : CardSessionRunnable { @@ -109,6 +111,14 @@ internal class ScanProductTask( } } + override fun preflightReadMode(): PreflightReadMode { + return if (shouldCheckIsAlreadyActivated) { + PreflightReadMode.FullCardReadWithAccessCodeCheck + } else { + return super.preflightReadMode() + } + } + private fun getErrorIfExcludedCard(cardDto: CardDTO, card: Card): TangemError? { if (cardDto.isExcluded) return TapSdkError.CardForDifferentApp() if (cardDto.isNotSupportedInThatRelease) return TapSdkError.CardNotSupportedByRelease() diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt index 2b20696ee9..0f7ba29f04 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt @@ -20,7 +20,10 @@ class FinalizeTwinTask( WriteProtectedIssuerDataTask(twinPublicKey, issuerKeys).run(session) { result -> when (result) { is CompletionResult.Success -> - PreflightReadTask(PreflightReadMode.FullCardRead).run(session) { readResult -> + PreflightReadTask( + readMode = PreflightReadMode.FullCardRead, + secureStorage = session.environment.secureStorage, + ).run(session) { readResult -> when (readResult) { is CompletionResult.Success -> ScanProductTask( @@ -28,6 +31,7 @@ class FinalizeTwinTask( derivationsFinder = null, visaCardScanHandler = null, visaCoroutineScope = null, + shouldCheckIsAlreadyActivated = false, onboardingV2FeatureToggles = null, ).run(session, callback) is CompletionResult.Failure -> diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index a0d719fc6b..283bf15238 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "releases-5.34-1430" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.34-579" +tangemCardSdk = "releases-5.34-580" #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 474d4b4fb4fad52ee447d60c566acb067b51cb6a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 19:51:42 +0300 Subject: [PATCH 085/106] 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 2d18650f6d..e4fac168bd 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 2d18650f6d4286353046ccb841745cef107c4fa6 +Subproject commit e4fac168bd941fe90b0f081dfa70ea1b4148b49f From cd504ed349a3d8c31a6aa773fd374bd229bac429 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 22:14:43 +0000 Subject: [PATCH 086/106] 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 4372af7f4b..283bf15238 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.33-1423" +tangemBlockchainSdk = "releases-5.34-1430" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.33-576" +tangemCardSdk = "releases-5.34-580" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-545" +tangemHotSdk = "develop-539" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From dcd728503ef1478de5a59ad15e3e897770aa090c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 11:17:10 +0200 Subject: [PATCH 087/106] Updated on 2026-08-14 --- tangem-android-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tangem-android-tools b/tangem-android-tools index 35755ad66f..43fab6f690 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 35755ad66f5f8fa6bcb1c84b443826cd315495d0 +Subproject commit 43fab6f690538391cae17e046ffb2ec9fe08b0c7 From 6e2ad61589872a2f136b0813fcc78c4b2885d210 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 16:36:14 +0500 Subject: [PATCH 088/106] Updated on 2026-08-14 --- .../presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f92476350d..11aaab2b47 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 @@ -33,7 +33,7 @@ sealed class WalletScreenAnalyticsEvent { put(AnalyticsParam.BALANCE, balance.value) tokensCount?.let { put(AnalyticsParam.TOKENS_COUNT, it.toString()) } }, - ) + ), AppsFlyerIncludedEvent class TokenBalance(balance: AnalyticsParam.EmptyFull, token: String) : Basic( event = "Token Balance", From 84153dd63b7c27e55d2ba314119e3b37602431fe Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 16:38:32 +0500 Subject: [PATCH 089/106] Updated on 2026-08-14 --- .../tap/di/domain/HotWalletDomainModule.kt | 9 ++++++ .../hotwallet/DefaultHotWalletRepository.kt | 7 ++--- .../CheckHotWalletUpgradeBannerUseCase.kt | 2 +- ...GetUpgradeBannerClosureTimestampUseCase.kt | 13 +++++++++ .../repository/HotWalletRepository.kt | 2 +- .../CheckHotWalletUpgradeBannerUseCaseTest.kt | 28 ++++++++++--------- .../domain/GetMultiWalletWarningsFactory.kt | 12 ++++++-- 7 files changed, 52 insertions(+), 21 deletions(-) create mode 100644 domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/GetUpgradeBannerClosureTimestampUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt index c99860711a..f3ee5bef10 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt @@ -3,6 +3,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.CloseHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase +import com.tangem.domain.hotwallet.GetUpgradeBannerClosureTimestampUseCase import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.hotwallet.IsAccessCodeSimpleUseCase import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase @@ -67,4 +68,12 @@ internal object HotWalletDomainModule { ): ShouldShowUpgradeHotWalletBannerUseCase { return ShouldShowUpgradeHotWalletBannerUseCase(hotWalletRepository) } + + @Provides + @Singleton + fun provideGetUpgradeBannerClosureTimestampUseCase( + hotWalletRepository: HotWalletRepository, + ): GetUpgradeBannerClosureTimestampUseCase { + return GetUpgradeBannerClosureTimestampUseCase(hotWalletRepository) + } } \ No newline at end of file diff --git a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt index dbbe66db29..c112597c17 100644 --- a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt +++ b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt @@ -53,10 +53,9 @@ internal class DefaultHotWalletRepository( } } - override suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long? { - return appPreferencesStore - .getObjectMapSync(PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY)[userWalletId.stringValue] - } + override fun upgradeBannerClosureTimestamp(userWalletId: UserWalletId): Flow = appPreferencesStore + .getObjectMap(PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY) + .map { it[userWalletId.stringValue] } override suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long?) { appPreferencesStore.editData { mutablePreferences -> diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt index 1038773b7c..28f7be6bd3 100644 --- a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt @@ -14,6 +14,7 @@ class CheckHotWalletUpgradeBannerUseCase( walletId: UserWalletId, hasBalance: Boolean, shouldShowUpgradeBanner: Boolean, + closureTimestamp: Long?, ): Either = try { val currentTime = System.currentTimeMillis() val creationTimestamp = hotWalletRepository.getWalletCreationTimestamp(walletId) @@ -27,7 +28,6 @@ class CheckHotWalletUpgradeBannerUseCase( creationTimestamp } - val closureTimestamp = hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(walletId) val daysSinceCreation = TimeUnit.MILLISECONDS.toDays(currentTime - creationTimestampActual) diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/GetUpgradeBannerClosureTimestampUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/GetUpgradeBannerClosureTimestampUseCase.kt new file mode 100644 index 0000000000..dbe9d2f2af --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/GetUpgradeBannerClosureTimestampUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.hotwallet + +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow + +class GetUpgradeBannerClosureTimestampUseCase( + private val hotWalletRepository: HotWalletRepository, +) { + operator fun invoke(userWalletId: UserWalletId): Flow { + return hotWalletRepository.upgradeBannerClosureTimestamp(userWalletId) + } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt index 01229d8d3a..2d63b01cde 100644 --- a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt @@ -17,7 +17,7 @@ interface HotWalletRepository { suspend fun setShouldShowUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) - suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long? + fun upgradeBannerClosureTimestamp(userWalletId: UserWalletId): Flow suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long?) diff --git a/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt index 6e415efea4..447864f896 100644 --- a/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt +++ b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt @@ -23,7 +23,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { @Test fun `GIVEN creation timestamp is null WHEN invoke THEN set timestamp and return false`() = runTest { coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns null - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -31,6 +30,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -42,7 +42,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN shouldShowUpgradeBanner is true and hasBalance WHEN invoke THEN return true`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -50,6 +49,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = true, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -60,7 +60,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN shouldShowUpgradeBanner is true WHEN invoke THEN return true regardless of balance`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -68,6 +67,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = true, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -79,7 +79,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -87,6 +86,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = false, + closureTimestamp = closureTimestamp, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -99,7 +99,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(15) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -107,7 +106,8 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = false, - ) + closureTimestamp = closureTimestamp, + ) assertThat(result).isInstanceOf(Either.Right::class.java) assertThat((result as Either.Right).value).isFalse() @@ -117,7 +117,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN no flags set and no closure and 30 days since creation WHEN invoke THEN return true`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -125,6 +124,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -135,7 +135,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN no flags set but less than 30 days since creation WHEN invoke THEN return false`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(15) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -143,6 +142,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -154,7 +154,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -162,6 +161,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = closureTimestamp, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -172,7 +172,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN first top-up detected WHEN invoke THEN return false and mark session`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -180,6 +179,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -194,7 +194,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN first top-up detected this session WHEN invoke THEN return false`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns true @@ -202,6 +201,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = true, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -212,7 +212,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN already had first top-up WHEN invoke with balance THEN do not set flags again`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -220,6 +219,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = true, + closureTimestamp = null, ) coVerify(exactly = 0) { hotWalletRepository.setHasHadFirstTopUp(any(), any()) } @@ -230,7 +230,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN multiple re-emissions with same state WHEN invoke THEN return same result`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -238,16 +237,19 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) val result2 = useCase( walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) val result3 = useCase( walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) assertThat((result1 as Either.Right).value).isTrue() 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 6218c6257a..a243ddef48 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 @@ -15,6 +15,7 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase +import com.tangem.domain.hotwallet.GetUpgradeBannerClosureTimestampUseCase import com.tangem.domain.hotwallet.ShouldShowUpgradeHotWalletBannerUseCase import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance @@ -62,10 +63,11 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val accountDependencies: AccountDependencies, private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, private val shouldShowUpgradeHotWalletBannerUseCase: ShouldShowUpgradeHotWalletBannerUseCase, + private val getUpgradeBannerClosureTimestampUseCase: GetUpgradeBannerClosureTimestampUseCase, private val checkHotWalletUpgradeBannerUseCase: CheckHotWalletUpgradeBannerUseCase, ) { - @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod") + @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod", "CastNullableToNonNullableType") fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver @@ -105,8 +107,10 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( .distinctUntilChanged(), shouldShowUpgradeHotWalletBannerUseCase.invoke(userWallet.walletId) .distinctUntilChanged(), + getUpgradeBannerClosureTimestampUseCase(userWallet.walletId) + .distinctUntilChanged(), ) { array -> array } - .combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = array) } + .combine(tokenListFlow()) { array, any: Any? -> arrayOf(any).plus(elements = array) } .map { array -> val lceTokens = array[0] as Lce>> val totalFiatBalance = lceTokens.map { it.first } @@ -119,6 +123,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val shouldAccessCodeSkipped = array[6] as Boolean val shouldShowYieldPromo = array[7] as Boolean val shouldShowUpgradeBanner = array[8] as Boolean + val closureTimestamp = array[9] as? Long buildList { addUsedOutdatedDataNotification(totalFiatBalance) @@ -130,6 +135,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( flattenCurrencies = flattenCurrencies, clickIntents = clickIntents, shouldShowUpgradeBanner = shouldShowUpgradeBanner, + closureTimestamp = closureTimestamp, ) addFinishWalletActivationNotification( @@ -469,6 +475,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( flattenCurrencies: Lce>, clickIntents: WalletClickIntents, shouldShowUpgradeBanner: Boolean, + closureTimestamp: Long?, ) { if (userWallet !is UserWallet.Hot) return @@ -479,6 +486,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( walletId = userWallet.walletId, hasBalance = hasBalance, shouldShowUpgradeBanner = shouldShowUpgradeBanner, + closureTimestamp = closureTimestamp, ).getOrNull() ?: return addIf( From 8b02a0825abe5285b43beae86417d50ad612d868 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 14:40:16 +0300 Subject: [PATCH 090/106] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 283bf15238..423909e1bc 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "releases-5.34-1430" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.34-580" +tangemCardSdk = "releases-5.34-581" #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 40da3bbe0fe1355832e5b6b1b1982cc51469cf92 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 18:48:48 +0400 Subject: [PATCH 091/106] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 56 ++++++++++------ .../data/pay/entity/WithdrawStoreData.kt | 1 + .../DefaultTangemPayWithdrawRepository.kt | 65 ++++++++++--------- .../data/pay/util/WithdrawStateConverter.kt | 1 + .../pay/util/WithdrawStoreDataConverter.kt | 1 + .../domain/pay/TangemPayWithdrawState.kt | 1 + 6 files changed, 74 insertions(+), 51 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 42450edff8..0d9a4eba92 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -51,6 +51,16 @@ internal class DefaultTangemPayStorage @Inject constructor( private val withdrawStoreDataConverter by lazy { WithdrawStoreDataConverter() } private val withdrawStateConverter by lazy { WithdrawStateConverter() } + private val listType by lazy { + Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java) + } + private val mapType by lazy { + Types.newParameterizedType(Map::class.java, String::class.java, listType) + } + private val adapter: JsonAdapter>> by lazy { + appPreferencesStore.moshi.adapter(mapType) + } + override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) { withContext(dispatcherProvider.io) { appPreferencesStore @@ -152,15 +162,19 @@ internal class DefaultTangemPayStorage @Inject constructor( } override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) { - val listType = Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java) - val mapType = Types.newParameterizedType(Map::class.java, String::class.java, listType) - val adapter: JsonAdapter>> = appPreferencesStore.moshi.adapter(mapType) appPreferencesStore.editData { prefs -> val walletKey = createWithdrawOrderIdKey(userWalletId) - val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]?.let(adapter::fromJson).orEmpty() - val updatedList = currentMap[walletKey].orEmpty() + withdrawStoreDataConverter.convert(data) - prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = adapter - .toJson(currentMap + (walletKey to updatedList)) + val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] + ?.let(adapter::fromJson) + .orEmpty() + val newItem = withdrawStoreDataConverter.convert(data) + val currentList = currentMap[walletKey].orEmpty() + val updatedList = buildList(currentList.size + 1) { + for (item in currentList) { if (item.orderId != newItem.orderId) add(item) } + add(newItem) + } + prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = + adapter.toJson(currentMap + (walletKey to updatedList)) } } @@ -170,9 +184,6 @@ internal class DefaultTangemPayStorage @Inject constructor( } override suspend fun getWithdrawOrders(userWalletId: UserWalletId): List { - val listType = Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java) - val mapType = Types.newParameterizedType(Map::class.java, String::class.java, listType) - val adapter: JsonAdapter>> = appPreferencesStore.moshi.adapter(mapType) val map = appPreferencesStore.data.firstOrNull() ?.get(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)?.let(adapter::fromJson).orEmpty() return map[createWithdrawOrderIdKey(userWalletId)].orEmpty().map(withdrawStateConverter::convert) @@ -190,15 +201,22 @@ internal class DefaultTangemPayStorage @Inject constructor( } override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) { - appPreferencesStore.editData { mutablePreferences -> - val orders = mutablePreferences.getObjectMap( - PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, - ) - .minus(createWithdrawOrderIdKey(userWalletId)) - mutablePreferences.setObjectMap( - key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, - value = orders, - ) + appPreferencesStore.editData { prefs -> + val walletKey = createWithdrawOrderIdKey(userWalletId) + val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]?.let(adapter::fromJson) + .orEmpty() + val currentList = currentMap[walletKey].orEmpty() + val updatedList = buildList(currentList.size) { + for (item in currentList) { + if (item.orderId != orderId) add(item) + } + } + val updatedMap = if (updatedList.isEmpty()) { + currentMap - walletKey + } else { + currentMap + (walletKey to updatedList) + } + prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = adapter.toJson(updatedMap) } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/entity/WithdrawStoreData.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/entity/WithdrawStoreData.kt index cdad87d12b..02d63db72f 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/entity/WithdrawStoreData.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/entity/WithdrawStoreData.kt @@ -7,6 +7,7 @@ import com.squareup.moshi.JsonClass data class WithdrawStoreData( @Json(name = "orderId") val orderId: String, @Json(name = "exchangeData") val exchangeData: ExchangeStoreData?, + @Json(name = "txHash") val txHash: String?, ) @JsonClass(generateAdapter = false) 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 922b617842..7bf21432a7 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 @@ -110,12 +110,15 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( val storeData = TangemPayWithdrawState( orderId = orderId, exchangeData = exchangeData, + txHash = withdrawTxHash, ) if (order != null && !withdrawTxHash.isNullOrEmpty()) { - finalizeWithdraw(userWallet = userWallet, txHash = withdrawTxHash, exchangeData = exchangeData) - .onLeft { - tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) - } + finalizeWithdraw( + userWallet = userWallet, + txHash = withdrawTxHash, + exchangeData = exchangeData, + orderId = orderId, + ) } else { tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) } @@ -124,6 +127,7 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( private suspend fun finalizeWithdraw( userWallet: UserWallet, + orderId: String, txHash: String, exchangeData: TangemPayWithdrawExchangeState, ): Either { @@ -135,10 +139,9 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( payInAddress = exchangeData.payInAddress, txHash = txHash, payInExtraId = exchangeData.payInExtraId, - ) - .onLeft { error -> - Timber.tag(TAG).e(error.toString()) - } + ).also { + tangemPayStorage.deleteWithdrawOrder(userWallet.walletId, orderId) + } } override suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean { @@ -168,19 +171,23 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( private suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet, data: TangemPayWithdrawState) { val exchangeData = data.exchangeData ?: return + val storedHash = data.txHash val orderId = data.orderId - val order = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() - ?: return - val txHash = order.withdrawTxHash + val txHash = if (storedHash.isNullOrEmpty()) { + val order = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + ?: return + order.withdrawTxHash.also { fetchedHash -> + tangemPayStorage.storeWithdrawOrder( + userWalletId = userWallet.walletId, + data = data.copy(txHash = fetchedHash), + ) + } + } else { + storedHash + } if (!txHash.isNullOrEmpty()) { - finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData) - .onRight { - tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) - } - .onLeft { - startWithdrawOrderPolling(userWallet = userWallet, orderId = orderId, exchangeData = exchangeData) - } + finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData, orderId = orderId) } else { startWithdrawOrderPolling(userWallet = userWallet, orderId = orderId, exchangeData = exchangeData) } @@ -204,20 +211,14 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( .onRight { order -> val txHash = order.withdrawTxHash if (txHash.isNullOrEmpty()) return@onRight - finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData) - .onRight { - tangemPayStorage.deleteWithdrawOrder( - userWalletId = userWallet.walletId, - orderId = orderId, - ) - withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } - return@launch - } - .onLeft { error -> - Timber.tag(TAG).e("finalizeWithdraw error: $error") - withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } - return@launch - } + finalizeWithdraw( + userWallet = userWallet, + txHash = txHash, + exchangeData = exchangeData, + orderId = orderId, + ) + withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } + return@launch } .onLeft { error -> Timber.tag(TAG).e("getOrderData error ${error.errorCode}") diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStateConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStateConverter.kt index 5bcefd3b8e..019ee19504 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStateConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStateConverter.kt @@ -18,5 +18,6 @@ class WithdrawStateConverter : Converter Date: Wed, 25 Feb 2026 18:27:23 +0300 Subject: [PATCH 092/106] Updated on 2026-08-14 --- .../exchangeServices/DefaultRampManager.kt | 28 +----- core/res/src/main/res/values-de/strings.xml | 8 +- core/res/src/main/res/values-es/strings.xml | 74 ++++++++------- core/res/src/main/res/values-fr/strings.xml | 8 +- core/res/src/main/res/values-it/strings.xml | 5 + core/res/src/main/res/values-ja/strings.xml | 14 ++- core/res/src/main/res/values-ru/strings.xml | 16 +++- .../src/main/res/values-uk-rUA/strings.xml | 22 ++++- .../src/main/res/values-zh-rTW/strings.xml | 5 + core/res/src/main/res/values/strings.xml | 13 ++- .../feature/swap/DefaultSwapRepository.kt | 93 +++++++++++++------ .../swap/domain/GetAvailablePairsUseCase.kt | 1 + .../feature/swap/domain/api/SwapRepository.kt | 1 + .../converters/AccountTokenItemConverter.kt | 7 +- .../swap/converters/TokensDataConverter.kt | 19 +--- .../swap/converters/TokensDataConverterV2.kt | 7 +- .../tangem/feature/swap/model/SwapModel.kt | 58 +++++++++--- .../swap/model/SwapNotificationsFactory.kt | 4 +- .../swap/models/states/SwapNotificationUM.kt | 9 +- .../tangem/feature/swap/ui/StateBuilder.kt | 7 +- 20 files changed, 248 insertions(+), 151 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index bd69d9835d..285f2c46e9 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -86,7 +86,7 @@ internal class DefaultRampManager( cryptoCurrency: CryptoCurrency, ): ScenarioUnavailabilityReason { val availabilityState = runSuspendCatching { - getExchangeableState(userWalletId, cryptoCurrency) + getExchangeableState() }.getOrNull() ?: ExpressAvailabilityState.Error return availabilityState.toReason(cryptoCurrency.name) } @@ -142,21 +142,9 @@ internal class DefaultRampManager( } } - private suspend fun getExchangeableState( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): ExpressAvailabilityState { - val asset = expressServiceFetcher.getInitializationStatus(userWalletId).firstOrNull() - ?: return ExpressAvailabilityState.Loading - return when (asset) { - is Lce.Error -> ExpressAvailabilityState.Error - is Lce.Loading -> ExpressAvailabilityState.Loading - is Lce.Content -> { - val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(assetId = it.id) } - foundAsset?.isExchangeAvailable?.toSwapAvailabilityState() - ?: ExpressAvailabilityState.AssetNotFound - } - } + private fun getExchangeableState(): ExpressAvailabilityState { + // In task [REDACTED_TASK_KEY], removed all checks to make all tokens available + return ExpressAvailabilityState.Available } private suspend fun getOnrampAvailableState( @@ -190,14 +178,6 @@ internal class DefaultRampManager( } } - private fun Boolean.toSwapAvailabilityState(): ExpressAvailabilityState { - return if (this) { - ExpressAvailabilityState.Available - } else { - ExpressAvailabilityState.NotExchangeable - } - } - private fun Boolean.toOnrampAvailabilityState(): ExpressAvailabilityState { return if (this) { ExpressAvailabilityState.Available diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 5ee7d6ff61..9903332ae6 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1496,7 +1496,7 @@ Ihre Karte ist entsperrt. Abhebung Auf gerooteten Geräten nicht nutzbar. - KYC abbrechen + KYC vom Hauptbildschirm ausblenden Guthaben hinzufügen Aufladeoptionen Kartennummer @@ -1562,10 +1562,10 @@ Status anzeigen KYC für Tangem Pay in Arbeit Dokumente werden in der Regel innerhalb von 5 Minuten automatisch überprüft. In seltenen Fällen, wenn eine manuelle Überprüfung erforderlich ist, kann es bis zu 48 Stunden dauern. - KYC abgelehnt + Abgelehnt KYC-Sperre ausblenden - Entschuldigung, wir konnten dies nicht überprüfen. - Dein Profil. + Leider konnten wir Ihre Identität + nicht verifizieren. Holen Sie sich Ihre kostenlose virtuelle Tangem Visa-Karte Nutzen Sie USDC für alltägliche Zahlungen Karte erhalten diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index fac79c1d1f..462fd75c00 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -4,7 +4,7 @@ Saltar de todos modos Código de acceso no establecido Cambiar código - Su código de acceso se utilizará para desbloquear su billetera y proteger el acceso a sus activos. + Su código de acceso desbloquea y protege el acceso a su billetera Usar de todos modos Este código de acceso se puede adivinar fácilmente. Introduzca el código de acceso @@ -399,6 +399,7 @@ Termine el staking 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 semana con @@ -468,9 +469,15 @@ Red de %s Envíe fondos utilizando solo Las mejores oportunidades + Limpiar filtro + La lista está temporalmente vacía porque se está actualizando. Vuelva a consultarla en un momento. Todas las redes Todos los tipos + Filtrar por + Mis redes + Redes Mayormente usado + Sin resultados Ganar Hola equipo de soporte, he encontrado un error con el código: %s Error de WalletConnect @@ -543,7 +550,7 @@ Comprando %s Comprando %s... Ocultar esta transacción - Una vez oculto, el estado de la transacción no se puede volver a ver. En su lugar, basta con deslizar el dedo para descartarla. + Si oculta esta transacción, ya no aparecerá en la pantalla de estado. Si solo quieres cerrar la pantalla de estado y volver más tarde, solo tiene que deslizar el dedo hacia otro lado. ¿Ocultar el estado de la transacción? Este token no es compatible. Por favor, elige un token diferente para intercambiar. %s no está soportado @@ -552,9 +559,9 @@ ID : %s ID de transacción copiado Convierta una de sus activos de su portafolio por este token - Una mayor velocidad significa una confirmación más rápida y una tarifa de red más alta. %s + Una mayor velocidad significa una confirmación más rápida\ny una tarifa de red más alta. %s Elija la velocidad - Elija qué token utilizar para pagar la tarifa de red. %s + Elige qué token utilizar para pagar\nla tarifa de red. %s Elija token Mercado y noticias De actualidad @@ -577,7 +584,7 @@ Ocurrió un error. Código: %s Requiere memo Esta transacción - Especifique el límite aprobado para el token seleccionado. + Al aprobar, permite que el contrato inteligente utilice sus tokens en transacciones futuras. Montante %s La función Aprobar es necesaria para otorgar permiso a otra dirección para usar una cantidad específica de sus tokens. Por diseño, los contratos inteligentes no pueden acceder a sus tokens sin su aprobación. Al \"desbloquear\" sus tokens, autoriza al contrato inteligente de StakeKit a usarlos. Los mineros de la red reciben una tarifa de gas (pagada por usted) por registrar esta acción en la cadena de bloques. Puede apostar su token después de dar su aprobación. Para continuar, debe autorizar el contrato inteligente de Polygon para utilizar su %s @@ -733,7 +740,7 @@ Sin datos Análisis del Mercado Acciones rápidas - Busque en el mercado + Buscar tokens Resultado Ver tokens con marketcap inferior a 100.000$ Mostrar tokens @@ -748,10 +755,10 @@ 7d Todo Compradores experimentados - Evaluación + Capitalización de mercado Ordenar por - Top Ganadores - Top Perdedores + Ganadores + Perdedores Tendencias Modo Rendimiento Staking es la forma más fácil de recibir recompensas por sus criptomonedas. %s @@ -801,7 +808,7 @@ Capital. de mercado El valor total de una criptomoneda calculado multiplicando su precio por la cantidad de monedas en circulación Capital. de mercado - Evaluación de mercado + Posición en el mercado Posición en la clasificación de criptomonedas entre todas las monedas según la capitalización de mercado Evaluación de mercado Suministro máximo @@ -823,6 +830,7 @@ Volumen Tire hacia arriba o toque la barra de búsqueda para agregar tokens directamente desde el mercado Agregar tokens + Añadir más tokens Potencie sus activos mientras los suministra con acceso inmediato. %s Activar Modo Rendimiento Debes actualizar a %1$s para crear una mobile wallet @@ -840,7 +848,7 @@ Resumen rápido Noticias Tokens relacionados - Fuentes + Noticias relacionadas Manténgase informado NFC no está disponible en su dispositivo Acerca de NFT @@ -927,9 +935,9 @@ Por favor repita la operación. La tarjeta/anillo se restablecerá a la configuración de fábrica. Error de activación Agregar tokens - Ha agregado una tarjeta/anillo de backup. Cuando el proceso de backup finalice, no podrá añadir más dispositivos de backup. Si tiene otra tarjeta o anillo, agréguela al backup. ¿Quiere continuar el proceso de backup? - El proceso de backup está parcialmente completo. No puede salir ahora. - La frase de contraseña es una característica de seguridad avanzada utilizada por las billeteras criptográficas. Agrega una palabra o frase adicional de su elección a su frase de recuperación ya existente para desbloquear un conjunto completamente nuevo de direcciones. + Ha añadido una tarjeta o anillo como copia de seguridad. Una vez finalizada la copia de seguridad, no puedes añadir más dispositivos. Si tiene una tarjeta o anillo más, añádalo ahora. ¿Quiere continuar? + La copia de seguridad está parcialmente completa y no se puede salir ahora. + Una frase de contraseña es una función de seguridad opcional que añade una palabra o frase a su frase de recuperación, creando un nuevo conjunto de direcciones de billetera para una mayor protección. Agregar una tarjeta o un anillo de backup Escanee la tarjeta Escanee la tarjeta núm. %d @@ -939,7 +947,7 @@ Continuar a mi billetera Finalizar el backup Recibir cripto - Escanee la tarjeta principal + Escanee la tarjeta principal o anillo Finalizar más tarde ¿Cómo funciona? Vamos a generar todas las claves en su tarjeta o anillos y crear una billetera segura @@ -993,7 +1001,7 @@ Ningunos dispositivos de backup Notificaciones Un dispositivo de backup agregado - Prepare su tarjeta + Prepare su tarjeta o anillo Dos dispositivos de backup agregados Para empezar, simplemente recargue la billetera con cualquier cantidad Para empezar, simplemente recargue la billetera con más de %1$s %2$s @@ -1028,7 +1036,7 @@ El monto de la compra no debe ser mayor a %s La cantidad a comprar debe ser como mínimo %s No hay proveedores disponibles para esta moneda - Más rápido + Procesamiento más rápido Pagar con Método de pago Disponible hasta %s @@ -1299,7 +1307,7 @@ Tasa de recompensa promedio ¿Cómo funciona el staking? %s beneficio estimado - Calificación de mercado + Posición en el mercado Métrica Según las reglas de la red %1$s, se pueden reclamar recompensas desde %2$s. Las cantidades a continuación se acreditarán en su cuenta al deshacer el staking. Mínimo requerido @@ -1412,7 +1420,7 @@ Sus stakes Almacene sus criptomonedas de forma segura manteniendo las claves privadas almacenadas en su tarjeta Billetera de hardware revolucionaria - Hasta 3 tarjetas físicas o anillos por billetera + Añade hasta 3 tarjetas o anillos a una billetera Backup ultra seguro Una billetera de hardware para su Bitcoin, Ethereum y muchas más monedas simultáneamente – todo en una sola tarjeta o anillo Miles de monedas @@ -1428,6 +1436,9 @@ La red cobrará una tasa de aprobación del token para verificar que usted autoriza el uso de su token para el intercambio. Intercambie más tokens a mejores tasas directamente en su billetera. ¡Nuevo proveedor de intercambio disponible! + ¿Busca algo más?\n¡Intente buscar o explorar otra criptomoneda! + Busque cualquier token, incluso si aún no está en su lista. + Utilice la búsqueda para encontrar lo que necesite Siéntase seguro con una asistencia permanente que le ayudará con cualquier problema Siempre aquí Múltiples proveedores de confianza en un solo lugar: intercambie cualquier activo sin esfuerzo en su billetera @@ -1493,7 +1504,7 @@ Tu tarjeta está descongelada. Retirada No se puede usar en un dispositivo rooteado - Cancelar KYC + Ocultar verificación de la pantalla Agregar fondos Opciones de recarga Número de tarjeta @@ -1554,15 +1565,15 @@ ¿Seguro que desea detener el proceso KYC? Puede retomarlo en cualquier momento. No pudimos verificar tu perfil. Si tienes alguna pregunta, contacta con el soporte. Lamentablemente, no pudimos verificar tu identidad - El proceso KYC ha fallado + KYC rechazado KYC en curso Ver estado KYC en progreso para Tangem Pay Los documentos suelen verificarse automáticamente en menos de 5 minutos. En casos excepcionales que requieran revisión manual, el proceso puede tardar hasta 48 horas. - KYC rechazado + Rechazado Ocultar el bloque KYC - Lo sentimos, no pudimos verificarle - su perfil. + Lo sentimos, no pudimos verificar + u identidad. Obtén tu tarjeta virtual Tangem Visa gratuita Usa USDC para pagos cotidianos Obtener tarjeta @@ -1621,7 +1632,7 @@ Cambie este token por otro por una tarifa de servicio de %1$s del %2$s al %3$s de febrero. Intercambie con Changelly, %s comisiones Intercambie ahora - Criptomonedas populares 🔥 + Tendencia del mercado 🔥 No disponible para compra No disponible para vender No disponible para cambio desde %s @@ -1670,7 +1681,7 @@ ¿Quiere utilizar\nnotificaciones push? Active las notificaciones push y le avisaremos al instante cuando le lleguen fondos. No se pierda ninguna transacción - Agregar una nueva billetera + Agregar billetera Si olvida esta billetera sin una copia de seguridad, perderá permanentemente el acceso a sus fondos. ¿Estás seguro de que deseas olvidar esta billetera? Ha ocurrido un error, por favor escanee su tarjeta o anillo para iniciar sesión @@ -1748,7 +1759,7 @@ Desbloquear Escanee su tarjeta para desbloquear el acceso Desbloqueo necesario - Elija cómo agregar su billetera + Elige tu tipo de billetera Escanee su tarjeta o anillo Tangem para restaurarlo o importarlo desde otra billetera. Crear una billetera de hardware ¿Quiere comprar una  Billetera Tangem? @@ -1838,7 +1849,7 @@ Use %s o escanee una tarjeta/anillo para desbloquear el acceso a su billetera El proceso de obtención de permisos está actualmente en marcha y se completará pronto. Aprobación en curso - Parece que la activación de la tarjeta no ha funcionado correctamente. Esto puede deberse a un problema con el módulo NFC de su dispositivo o a una mala conexión de la tarjeta con su dispositivo. Comuníquese con nuestro equipo de soporte para obtener ayuda. + La activación no se completó correctamente. Esto podría deberse a un problema de NFC o a una lectura incorrecta. Contacte con nuestro equipo de soporte para obtener ayuda. Error de activación El 3 de diciembre de 2024, la red BEP-2 fue desactivada por decisión de los desarrolladores de la red y ya no es compatible BNB Beacon Chain se cerrará @@ -1851,6 +1862,7 @@ Actualizar Iniciar migración Copiar + Para mantener el acceso a sus fondos, comience la migración de acuerdo con las pautas oficiales de Clore. La firma de mensajes no es compatible con esta red No se puede firmar el mensaje. Por favor, inténtelo de nuevo. Según la documentación oficial de Clore, todas las monedas recibidas antes del 21 de diciembre serán migradas a Clore (token ERC-20); las monedas recibidas después de esa fecha no lo serán. Se está desarrollando una solución de transferencia — mantente atento. @@ -1873,7 +1885,7 @@ Aprobación en proceso El monto mínimo de transacción es %1$s. Asegúrese de que el saldo restante después del canje no sea inferior a %2$s. No tienes tokens en tu portafolio a los que puedas intercambiar %s. Por favor, añade otro token para realizar el intercambio. - No hay tokens disponibles para intercambiar + No se agregaron tokens compatibles Para realizar una transacción necesita depositar %1$s %2$s No se pueden cubrir %s tarifa El monto a recibir debe ser de al menos %s @@ -2045,11 +2057,11 @@ Crear o importar una billetera de software Cree o importe una billetera de software en su teléfono. Empezar con Mobile Wallet - Otro método + Otros métodos Utilice la billetera de hardware Tangem Obtenga más información y compre Ignorar - Tiene un backup interrumpido. ¿Quiere reanudarlo? + Su copia de seguridad se ha interrumpido. ¿Desea reanudarla? Sí, reanudar Descartar Si descarta el backup ahora, tendrá que restablecer los dispositivos a los ajustes de fábrica para empezar de nuevo diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index a89fafa94f..fcdf84f726 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1499,7 +1499,7 @@ Votre carte est dégelée. Retrait Impossible à utiliser sur un appareil rooté - Annuler KYC + Masquer la vérification de l\'écran Ajouter des fonds Options de recharge Numéro de carte @@ -1565,10 +1565,10 @@ Voir le statut KYC en cours pour Tangem Pay Les documents sont généralement vérifiés automatiquement en moins de 5 minutes. Dans de rares cas nécessitant une vérification manuelle, le processus peut prendre jusqu’à 48 heures. - KYC rejeté + Refusé Masquer le bloc KYC - Désolé, nous n\'avons pas pu vérifier. - votre profil. + Désolé, nous n\'avons pas pu vérifier + votre identité. Obtenez votre carte virtuelle Tangem Visa gratuite Utilisez USDC pour les paiements quotidiens Obtenir la carte diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 50290eaea8..9cd39c1c7a 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -98,6 +98,7 @@ Impossibile sbloccare la carta. Riprova più tardi. La tua carta è sbloccata. Prelievo + Nascondi verifica dalla schermata Aggiungi fondi Opzioni di ricarica Numero carta @@ -151,10 +152,14 @@ Tangem Pay Non siamo riusciti a verificare il tuo profilo. Per domande, contatta il supporto. Purtroppo non siamo riusciti a verificare la tua identità + KYC rifiutato KYC in corso Visualizza stato KYC in corso per Tangem Pay I documenti vengono solitamente verificati automaticamente entro 5 minuti. In rari casi, se è necessaria una revisione manuale, il processo può richiedere fino a 48 ore. + Rifiutato + Spiacenti, non siamo riusciti a verificare + la tua identità. Ottieni la tua carta virtuale Tangem Visa gratuita Usa USDC per i pagamenti quotidiani Ottieni carta diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 46949f43d3..6b87995090 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -392,6 +392,7 @@ ステーキング解除 %1$sの制限により、1つのトランザクションに収まるUTXOは%2$d個のみです。つまり、 %3$s以下しか送信できません。量を減らす必要があります。 値がコピーされました + ウォレット はい @@ -1416,6 +1417,9 @@ ネットワークは、あなたがトークンのスワップを承認していることを確認するために、トークン承認手数料を請求します。 より多くのトークンをより良いレートで、ウォレット内にて直接交換します。 新しいスワッププロバイダーが利用可能になりました! + 他のものをお探しですか?\n検索してみるか、別の暗号資産をチェックしてみましょう! + どのトークンでも検索できます。まだ一覧に表示されていないものでも検索可能です。 + 必要なものは検索して見つけましょう。 24時間体制のサポートであらゆる問題に対応します。 いつもここに 複数の信頼できるプロバイダーが一箇所に集結。ウォレット内で様々な暗号資産を簡単に交換できます。 @@ -1481,7 +1485,7 @@ カードの凍結が解除されました 出金 Root化された端末では使用できません - KYCをキャンセル + メイン画面からKYCを非表示にする 資金を追加 入金オプション カード番号 @@ -1542,15 +1546,15 @@ KYC手続きを中止しますか?いつでも再開できます。 プロフィールを確認できませんでした。ご不明な点があればサポートまでお問い合わせください。 申し訳ございませんが、本人確認を行うことができませんでした - KYCの認証に失敗しました + KYCが承認されませんでした KYC進行中 ステータスを表示 Tangem PayのKYC手続き進行中 書類は通常、5分以内に自動で確認されます。手動での審査が必要な稀な場合は、最大48時間かかることがあります。 - KYCが承認されませんでした + 拒否されました KYCブロックを非表示 - 申し訳ありません、確認できませんでした - あなたのプロフィール。 + 申し訳ございませんが、 + 本人確認ができませんでした。 無料のTangem Visaバーチャルカードを入手 日常の支払いにUSDCを利用 カードをGET diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 550f256f3f..5a0683d679 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -31,7 +31,7 @@ Этот аккаунт не может быть архивирован. Мы не смогли создать аккаунт. Пожалуйста, попробуйте позже. Аккаунт создан - Архивный аккаунт + Архивировать аккаунт Архив Вы архивируете свой аккаунт, но в любое время можете вернуть его обратно Архивация... @@ -412,6 +412,7 @@ Завершить стейкинг Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. Значение скопировано + Кошельки неделю с Да @@ -482,6 +483,7 @@ Отправляйте средства, используя только Лучшие возможности Очистить фильтр + Список временно пуст — он обновляется. Пожалуйста, зайдите чуть позже. Все сети Все типы Фильтровать по @@ -1466,6 +1468,8 @@ Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. Обменивайте больше токенов по лучшим курсам прямо в вашем кошельке. Новый провайдер обмена! + Найдите любой токен, даже если его ещё нет в вашем списке + Используйте поиск, чтобы найти то, что вам нужно. Чувствуйте уверенность с круглосуточной поддержкой, готовой помочь в любой ситуации! Круглосуточная поддержка Надежные провайдеры в одном месте — обменивайте любые активы легко и быстро прямо в своем кошельке! @@ -1511,6 +1515,8 @@ Заморозить Карта заморожена Обратиться в поддержку + %s・%s + MCC %s Другое Невозможно использовать на устройствах с root-доступом. Успешно завершено @@ -1528,7 +1534,7 @@ Карта разморожена Вывести Запрещено использовать на root-устройствах - Отменить KYC + Скрыть KYC с главной Пополнить Способы пополнения Номер @@ -1588,11 +1594,15 @@ Если вы отмените KYC сейчас, вы сможете потом начать его заново Не удалось подтвердить ваш профиль. Если у вас есть вопросы, обратитесь в службу поддержки. К сожалению, нам не удалось подтвердить вашу личность + KYC отклонено KYC в процессе Посмотреть статус KYC в процессе для Tangem Pay Обычно документы проверяются автоматически в течение 5 минут, в редких случах при необходимости ручной проверки – до 48 часов. - KYC отклонено + Отклонено + Скрыть KYC с главной + Извините, мы не смогли подтвердить + ваш профиль. Откройте бесплатную виртуальную карту Tangem Visa Оплачивайте ежедневные покупки в USDC Открыть карту 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 fa1ef254d0..94cc71f5db 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -413,6 +413,7 @@ Скасувати стейкінг Через обмеження %1$s в одну транзакцію може поміститися тільки %2$d UTXO. Це означає, що ви можете відправити тільки %3$s або менше. Вам потрібно зменшити суму. Скопійовано + Гаманці тиждень з Так @@ -483,6 +484,7 @@ Надсилайте кошти, використовуючи лише Найкращі можливості Очистити фільтр + Список тимчасово порожній, оскільки триває оновлення. Спробуйте ще раз за мить. Всі мережі Всі типи Фільтрувати за @@ -490,6 +492,7 @@ Мережі Часто використовані Немає результатів + Заробляйте Привіт, команда підтримки, я зіткнувся з помилкою з кодом: %s Помилка WalletConnect Ви використали картку або кільце від іншого гаманця. Прикладіть картку або кільце, пов\'язану з цим гаманцем. @@ -853,6 +856,7 @@ Оновіться до версії %1$s, щоб створити мобільний гаманець Мобільний гаманець потребує %1$s або новіше. Всі новини + Вподобайка %d годину тому %d години тому @@ -865,7 +869,10 @@ %d хвилин тому %d хвилин тому + Короткий огляд + Новини Пов\'язані токени + Схожі новини Залишайтеся в курсі подій Функція NFC недоступна на вашому пристрої Про NFT @@ -1324,6 +1331,7 @@ Сума стейкінгу буде округлена до %1$s TRX відповідно до правил мережі. Сума зняття зі стейкінгу буде округлена до %1$s TRX через мережеві правила. APR %1$s%% + Ваші винагороди за стейкінг почнуть надходити через 5 епох (~25 днів), поки ваша делегація реєструється та враховується мережею. Зняти кошти Комісія за стейкінг-акаунт Стейкінг-акаунт — це спеціальний рахунок, на якому зберігаються застейкані SOL токени. Він створюється, коли ви делегуєте свої токени валідатору для участі у перевірці транзакцій та отримання винагород. За створення стейкінг-акаунту стягується невелика комісія, яка повертається після завершення стейкінгу. @@ -1334,6 +1342,7 @@ APR APY Винагороди автоматично накопичуються на вашому балансі щодня. + Винагороди реінвестуються у ваш баланс стейкінгу. Зароблено коштів: %s Доступно Середня ставка винагороди Що таке стейкінг? @@ -1467,6 +1476,9 @@ Мережа стягує комісію за схвалення токену за підтвердження, що саме ви дозволяєте використовувати ваш токен для обміну. Обмінюйте більше токенів за вигіднішим курсом прямо у своєму гаманці. З\'явився новий провайдер обмінів! + Шукаєте щось інше?\nСпробуйте пошукати або перегляньте інші криптовалюти! + Шукайте будь-який токен, навіть якщо його ще немає у вашому списку. + Використовуйте пошук, щоб знайти потрібне Надійна підтримка 24/7, щоб ваші фінансові операції були швидкими та надійними! Цілодобова підтримка Надійні провайдери дозволяють легко обмінювати активи, забезпечуючи повну безпеку у вашому гаманці @@ -1513,6 +1525,8 @@ Вашу картку заморожено. Звернутися до підтримки Причина: %s + %s.%s + MCC %s Інше Неможливо використовувати на пристроях з root-правами. Завершено @@ -1530,7 +1544,7 @@ Картку розморожено. Виведення коштів Заборонено використовувати на root-пристроях - Скасувати KYC + Приховати KYC з головного екрана Поповнити рахунок Варіанти поповнення Номер картки @@ -1567,6 +1581,7 @@ Обміняйте будь-який актив у вашому портфелі на картку Реквізити картки Розморозити картку + Поверніться до додатка, якщо ви забудете його. Ваш ПІН Вивести Вивід наразі недоступний @@ -1590,10 +1605,15 @@ Ви впевнені, що хочете зупинити процес KYC? Ви можете повернутися до нього в будь-який момент. Не вдалося підтвердити ваш профіль. Якщо у вас є питання, зверніться у службу підтримки. На жаль, нам не вдалося підтвердити вашу особу + KYC відхилено KYC в процесі Переглянути статус Перевірка KYC для Tangem Pay в процесі Документи зазвичай перевіряються автоматично протягом 5 хвилин. У рідкісних випадках, коли потрібна ручна перевірка, це може зайняти до 48 годин. + Відхилено + Приховати блок KYC + Вибачте, ми не змогли підтвердити + вашу особу. Отримайте безкоштовну віртуальну картку Tangem Visa Використовуйте USDC для щоденних платежів Отримати картку 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 f2a8057772..c410e833bc 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -341,6 +341,7 @@ 無法解凍卡片。請稍後再試。 您的卡片已解凍。 提現 + 在主畫面隱藏身份驗證 添加资金 充值选项 卡號 @@ -394,10 +395,14 @@ Tangem Pay 我们无法验证您的资料。如有任何疑问,请联系客服。 很抱歉,我们无法验证您的身份 + KYC 被拒絕 KYC进行中 查看状态 Tangem Pay 的 KYC 正在進行中 文件通常會在 5 分鐘內自動驗證完成。如需人工審核的特殊情況,可能需要最長 48 小時。 + 已拒絕 + 抱歉,我們無法驗證 + 您的身份 獲取您的免費 Tangem Visa 虛擬卡 使用 USDC 進行日常支付 获取卡片 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 55ca27cb0a..dbc8f644d4 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -399,6 +399,7 @@ Unstake 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 week with Yes @@ -1436,7 +1437,9 @@ The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. Exchange more tokens at better rates directly in your wallet. New Swap Provider Available! - Looking for something else?/nTry searching or explore another crypto! + Looking for something else?\nTry searching or explore another crypto! + Search for any token, even if it’s not in your list yet. + Use search to find what you need Feel confident with round-the-clock support to help with any issues Always Here Multiple trusted providers in one place—swap any asset effortlessly in your wallet @@ -1502,7 +1505,7 @@ Your card is unfrozen. Withdrawal Unable to use on rooted device - Cancel KYC + Hide KYC from main screen Add funds Top-up options Card Number @@ -1563,12 +1566,12 @@ Are you sure you want to stop the KYC process? You can return to it anytime. We could not verify your profile. If you have any questions, please contact support. Unfortunately, we couldn\'t verify your identity - KYC has failed + KYC rejected KYC in progress View Status KYC in progress for Tangem Pay Documents are usually verified automatically within 5 minutes. In rare cases, if manual review is required, it may take up to 48 hours. - KYC rejected + Rejected Hide KYC block Sorry, we couldn\'t verify your profile. @@ -1894,6 +1897,8 @@ Service temporarily unavailable The amount of tokens to be swapped must not exceed %s The amount to swap must be at least %s + Swapping isn\'t available for this pair. Please select a different token and try again. + Unsupported token pair Please change the amount to swap This card might be a production sample or counterfeit Authenticity check failed 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 ef5dc2f371..3146309d54 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 @@ -40,6 +40,7 @@ import com.tangem.feature.swap.domain.models.createFromAmountWithOffset import com.tangem.feature.swap.domain.models.domain.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async +import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext import timber.log.Timber import java.io.IOException @@ -130,21 +131,48 @@ internal class DefaultSwapRepository( userWallet: UserWallet, initialCurrency: LeastTokenInfo, currencyList: List, + isIgnoreExpress: Boolean, ): PairsWithProviders { return withContext(coroutineDispatcher.io) { - try { - val initial = NetworkLeastTokenInfo( - contractAddress = initialCurrency.contractAddress, - network = initialCurrency.network, - ) - val currenciesList = currencyList - .filter { currency -> - val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, currency) - val isAvailableForSwap = rampStateManager.checkAssetRequirements(requirements) - isAvailableForSwap - } - .map { currency -> leastTokenInfoConverter.convert(currency) } + val currenciesList = filterByAssetRequirements(userWallet, currencyList) + if (isIgnoreExpress) { + buildLocalPairs(initialCurrency, currenciesList) + } else { + fetchExpressPairs(userWallet, initialCurrency, currenciesList) + } + } + } + + private fun buildLocalPairs( + initialCurrency: LeastTokenInfo, + currenciesList: List, + ): PairsWithProviders { + val pairs = currenciesList.map { tokenInfo -> + SwapPairLeast( + from = initialCurrency, + to = LeastTokenInfo( + contractAddress = tokenInfo.contractAddress, + network = tokenInfo.network, + ), + providers = emptyList(), + ) + } + return PairsWithProviders(pairs = pairs, allProviders = emptyList()) + } + + private suspend fun fetchExpressPairs( + userWallet: UserWallet, + initialCurrency: LeastTokenInfo, + currenciesList: List, + ): PairsWithProviders { + try { + val initial = NetworkLeastTokenInfo( + contractAddress = initialCurrency.contractAddress, + network = initialCurrency.network, + ) + + val allPairs = supervisorScope { val pairsDeferred = async { getPairsInternal( userWallet = userWallet, @@ -161,27 +189,36 @@ internal class DefaultSwapRepository( ) } - val pairs = pairsDeferred.await().getOrThrow() - val reversedPairs = reversedPairsDeferred.await().getOrThrow() + pairsDeferred.await().getOrThrow() + reversedPairsDeferred.await().getOrThrow() + } - val allPairs = pairs + reversedPairs - - return@withContext swapPairInfoConverter.convert( - SwapPairsWithProviders( - swapPair = allPairs, - providers = emptyList(), - ), - ) - } catch (exception: Exception) { - if (exception is ApiResponseError.HttpException) { - throw ExpressException(errorsDataConverter.convert(exception.errorBody.orEmpty())) - } else { - throw exception - } + return swapPairInfoConverter.convert( + SwapPairsWithProviders( + swapPair = allPairs, + providers = emptyList(), + ), + ) + } catch (exception: Exception) { + if (exception is ApiResponseError.HttpException) { + throw ExpressException(errorsDataConverter.convert(exception.errorBody.orEmpty())) + } else { + throw exception } } } + private suspend fun filterByAssetRequirements( + userWallet: UserWallet, + currencyList: List, + ): List { + return currencyList + .filter { currency -> + val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, currency) + rampStateManager.checkAssetRequirements(requirements) + } + .map { currency -> leastTokenInfoConverter.convert(currency) } + } + private suspend fun getPairsInternal( userWallet: UserWallet, from: List, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/GetAvailablePairsUseCase.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/GetAvailablePairsUseCase.kt index 8d51f01469..c0bfc9041f 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/GetAvailablePairsUseCase.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/GetAvailablePairsUseCase.kt @@ -19,6 +19,7 @@ class GetAvailablePairsUseCase( userWallet = userWallet, initialCurrency = initialCurrency, currencyList = currencies, + isIgnoreExpress = true, ).pairs } } \ No newline at end of file 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 ee0b2065d3..a23260fb53 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 @@ -22,6 +22,7 @@ interface SwapRepository { userWallet: UserWallet, initialCurrency: LeastTokenInfo, currencyList: List, + isIgnoreExpress: Boolean = false, ): PairsWithProviders suspend fun getExchangeStatus(userWallet: UserWallet, txId: String): Either 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 adcecd045b..919e908498 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 @@ -36,11 +36,8 @@ internal class AccountTokenItemConverter( isExpanded = true, isCollapsable = false, tokens = value.currencyList.map { accountSwapCurrency -> - if (accountSwapCurrency.isAvailable) { - createAvailableItemConverter() - } else { - createUnavailableItemConverter() - }.convert(accountSwapCurrency.cryptoCurrencyStatus) + createAvailableItemConverter() + .convert(accountSwapCurrency.cryptoCurrencyStatus) }.map(TokensListItemUM::Token).toPersistentList(), ) } 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 ba936978ec..791af35a8c 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 @@ -17,6 +17,7 @@ import com.tangem.feature.swap.models.TokenToSelectState import com.tangem.feature.swap.presentation.R import com.tangem.utils.Provider import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList internal class TokensDataConverter( @@ -31,14 +32,9 @@ internal class TokensDataConverter( val availableTitle = TokenToSelectState.Title( resourceReference(R.string.exchange_tokens_available_tokens_header), ) - val unavailableTitle = TokenToSelectState.Title( - resourceReference( - R.string.exchange_tokens_unavailable_tokens_header, - wrappedList(value.fromCurrency.name), - ), - ) + val allTokens = group.available + group.unavailable return SwapSelectTokenStateHolder( - availableTokens = group.available.map { tokenWithBalanceToTokenToSelect(it, true) } + availableTokens = allTokens.map { tokenWithBalanceToTokenToSelect(it, true) } .toMutableList() .apply { if (this.isNotEmpty()) { @@ -46,14 +42,7 @@ internal class TokensDataConverter( } } .toImmutableList(), - unavailableTokens = group.unavailable.map { tokenWithBalanceToTokenToSelect(it, false) } - .toMutableList() - .apply { - if (this.isNotEmpty()) { - this.add(0, unavailableTitle) - } - } - .toImmutableList(), + unavailableTokens = persistentListOf(), tokensListData = TokenListUMData.EmptyList, onSearchEntered = onSearchEntered, onTokenSelected = onTokenSelected, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt index af538747a6..668b30aa88 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt @@ -46,11 +46,8 @@ internal class TokensDataConverterV2( } else { val tokensList = accountList.flatMap { (_, currencyList) -> currencyList.asSequence().map { accountSwapCurrency -> - if (accountSwapCurrency.isAvailable) { - accountListItemConverter.createAvailableItemConverter() - } else { - accountListItemConverter.createUnavailableItemConverter() - }.convert(accountSwapCurrency.cryptoCurrencyStatus) + accountListItemConverter.createAvailableItemConverter() + .convert(accountSwapCurrency.cryptoCurrencyStatus) }.map(TokensListItemUM::Token).toPersistentList() }.toPersistentList() diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 38be62641a..2727fa50c7 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 @@ -632,21 +632,14 @@ internal class SwapModel @Inject constructor( tokensDataState = state, ) - if (!isTokenAvailableForSwap(state, selectedCurrency, isReverseFromTo)) { - analyticsEventHandler.send( - SwapEvents.NoticeUnavailableToSwapPair( - sendToken = fromCurrencyStatus.currency.symbol, - receiveToken = toCurrencyStatus.currency.symbol, - sendBlockchain = fromCurrencyStatus.currency.network.name, - receiveBlockchain = toCurrencyStatus.currency.network.name, - ), - ) - uiState = stateBuilder.createSwapNotSupportedState( - uiStateHolder = uiState, + if (handleSwapNotSupported( + state = state, fromToken = fromCurrencyStatus, toToken = toCurrencyStatus, + fromAccount = fromAccount, toAccount = toAccount, ) + ) { return } @@ -1404,6 +1397,17 @@ internal class SwapModel @Inject constructor( toAccount = toAccount, selectedProvider = null, ) + swapRouter.openScreen(SwapNavScreen.Main) + if (handleSwapNotSupported( + state = tokens, + fromToken = fromToken, + toToken = toToken, + fromAccount = fromAccount, + toAccount = toAccount, + ) + ) { + return + } modelScope.launch { updateFeePaidCryptoCurrencyFor(fromToken) } @@ -1416,7 +1420,6 @@ internal class SwapModel @Inject constructor( reduceBalanceBy = lastReducedBalanceBy.value, toProvidersList = findSwapProviders(fromToken, toToken), ) - swapRouter.openScreen(SwapNavScreen.Main) updateTokensState(tokens) } } @@ -1943,6 +1946,37 @@ internal class SwapModel @Inject constructor( .orEmpty() } + /** + * @return true if swap is not supported and UI was updated to show error state + */ + private fun handleSwapNotSupported( + state: TokensDataStateExpress, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, + fromAccount: Account.CryptoPortfolio?, + toAccount: Account.CryptoPortfolio?, + ): Boolean { + val selectedCurrency = if (isOrderReversed) fromToken else toToken + if (isTokenAvailableForSwap(state, selectedCurrency, isOrderReversed)) return false + + analyticsEventHandler.send( + SwapEvents.NoticeUnavailableToSwapPair( + sendToken = fromToken.currency.symbol, + receiveToken = toToken.currency.symbol, + sendBlockchain = fromToken.currency.network.name, + receiveBlockchain = toToken.currency.network.name, + ), + ) + uiState = stateBuilder.createSwapNotSupportedState( + uiStateHolder = uiState, + fromToken = fromToken, + toToken = toToken, + fromAccount = fromAccount, + toAccount = toAccount, + ) + return true + } + private fun isTokenAvailableForSwap( state: TokensDataStateExpress, selectedCurrency: CryptoCurrencyStatus, 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 fdb774f17d..d57d2943f2 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 @@ -64,9 +64,9 @@ internal class SwapNotificationsFactory( ) } - fun getSwapNotSupportedNotifications(tokenName: String): ImmutableList { + fun getSwapNotSupportedNotifications(): ImmutableList { return persistentListOf( - SwapNotificationUM.Warning.SwapNotSupported(tokenName), + SwapNotificationUM.Warning.SwapNotSupported, ) } 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 7e5209dca5..e158fd5f3b 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 @@ -132,15 +132,12 @@ internal object SwapNotificationUM { ), ) - data class SwapNotSupported( - val tokenName: String, - ) : Warning( + data object SwapNotSupported : Warning( title = resourceReference( - id = com.tangem.feature.swap.presentation.R.string.express_swap_not_supported_title, - formatArgs = wrappedList(tokenName), + id = com.tangem.feature.swap.presentation.R.string.warning_express_unsupported_pair_title, ), subtitle = resourceReference( - com.tangem.feature.swap.presentation.R.string.express_swap_not_supported_text, + com.tangem.feature.swap.presentation.R.string.warning_express_unsupported_pair_description, ), ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index adeed21292..52bc5915e7 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 @@ -204,12 +204,15 @@ internal class StateBuilder( uiStateHolder: SwapStateHolder, fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, + fromAccount: Account.CryptoPortfolio?, toAccount: Account.CryptoPortfolio?, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( - type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable), + type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable).copy( + accountTitleUM = getFromCardAccountTitle(fromAccount), + ), amountTextFieldValue = null, amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", token = fromToken, @@ -240,7 +243,7 @@ internal class StateBuilder( networkIconRes = getActiveIconRes(toToken.currency.network.rawId), isBalanceHidden = isBalanceHiddenProvider(), ), - notifications = notificationsFactory.getSwapNotSupportedNotifications(toToken.currency.name), + notifications = notificationsFactory.getSwapNotSupportedNotifications(), fee = FeeItemState.Empty, swapButton = SwapButton( walletInteractionIcon = walletInterationIcon(userWalletProvider()), From 1806ddc31d6370c620b766c35683ff56fb96fbd8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Feb 2026 21:35:53 +0700 Subject: [PATCH 093/106] Updated on 2026-08-14 --- .../tap/domain/scanCard/DefaultScanCardProcessor.kt | 4 ++++ .../tap/domain/scanCard/LegacyScanProcessor.kt | 8 +++++++- .../repository/DefaultScanCardRepository.kt | 7 ++++++- .../tap/domain/sdk/impl/DefaultTangemSdkManager.kt | 3 ++- .../tap/domain/sdk/impl/MockTangemSdkManager.kt | 1 + .../repository/DefaultUserWalletsListRepository.kt | 4 +++- .../ui/cardsettings/model/CardSettingsModel.kt | 1 + .../com/tangem/domain/card/ScanCardProcessor.kt | 2 ++ .../com/tangem/domain/card/ScanCardUseCase.kt | 13 +++++++++---- .../domain/card/repository/ScanCardRepository.kt | 6 +++++- .../createwalletstart/CreateWalletStartModel.kt | 1 + .../features/details/utils/UserWalletSaver.kt | 1 + .../tangem/features/home/impl/model/HomeModel.kt | 1 + .../CreateHardwareWalletModel.kt | 1 + .../hotwallet/upgradewallet/UpgradeWalletModel.kt | 2 +- .../features/welcome/impl/model/WelcomeModel.kt | 1 + gradle/tangem_dependencies.toml | 2 +- .../kotlin/com/tangem/sdk/api/TangemSdkManager.kt | 8 ++++++++ 18 files changed, 55 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt index 216af3ca6d..f04b03cef6 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt @@ -20,6 +20,7 @@ internal class DefaultScanCardProcessor( cardId: String?, allowsRequestAccessCodeFromRepository: Boolean, analyticsSource: AnalyticsParam.ScreensSources, + shouldCheckIsAlreadyActivated: Boolean, ): CompletionResult { return if (isNewCardScanningEnabled) { UseCaseScanProcessor.scan(cardId, allowsRequestAccessCodeFromRepository) @@ -28,6 +29,7 @@ internal class DefaultScanCardProcessor( analyticsSource = analyticsSource, cardId = cardId, allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, + shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated, ) } } @@ -35,6 +37,7 @@ internal class DefaultScanCardProcessor( @Suppress("LongParameterList") override suspend fun scan( analyticsSource: AnalyticsParam.ScreensSources, + shouldCheckIsAlreadyActivated: Boolean, cardId: String?, onProgressStateChange: suspend (showProgress: Boolean) -> Unit, onWalletNotCreated: suspend () -> Unit, @@ -56,6 +59,7 @@ internal class DefaultScanCardProcessor( } else { legacyScanProcessor.scan( analyticsSource = analyticsSource, + shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated, cardId = cardId, onProgressStateChange = onProgressStateChange, onWalletNotCreated = onWalletNotCreated, 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 ad63237780..fd2e386a76 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 @@ -55,10 +55,12 @@ internal class LegacyScanProcessor @Inject constructor( cardId: String? = null, allowsRequestAccessCodeFromRepository: Boolean = false, analyticsSource: AnalyticsParam.ScreensSources, + shouldCheckIsAlreadyActivated: Boolean, ): CompletionResult { return tangemSdkManager.scanProduct( cardId = cardId, allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, + shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated, ) .doOnFailure { error -> onScanFailure(analyticsSource = analyticsSource, error = error, onFailure = {}, onCancel = {}) @@ -68,6 +70,7 @@ internal class LegacyScanProcessor @Inject constructor( @Suppress("LongParameterList") suspend fun scan( analyticsSource: AnalyticsParam.ScreensSources, + shouldCheckIsAlreadyActivated: Boolean, cardId: String?, onProgressStateChange: suspend (showProgress: Boolean) -> Unit, onWalletNotCreated: suspend () -> Unit, @@ -80,7 +83,10 @@ internal class LegacyScanProcessor @Inject constructor( tangemSdkManager.changeDisplayedCardIdNumbersCount(null) - val result = tangemSdkManager.scanProduct(cardId) + val result = tangemSdkManager.scanProduct( + cardId = cardId, + shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated, + ) val analyticsEvent = Basic.CardWasScanned(analyticsSource) store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result, analyticsSource)) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt index 1a4b50cc5a..ecd2cffeb0 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt @@ -13,11 +13,16 @@ internal class DefaultScanCardRepository( private val exceptionConverter = ScanCardExceptionConverter() - override suspend fun scanCard(cardId: String?, allowRequestAccessCodeFromStorage: Boolean): ScanResponse { + override suspend fun scanCard( + cardId: String?, + allowRequestAccessCodeFromStorage: Boolean, + shouldCheckIsAlreadyActivated: Boolean, + ): ScanResponse { return when ( val result = tangemSdkManager.scanProduct( cardId = cardId, allowsRequestAccessCodeFromRepository = allowRequestAccessCodeFromStorage, + shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated, ) ) { is CompletionResult.Success -> result.data diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index a9c061c5f7..aea6c8a351 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -166,6 +166,7 @@ internal class DefaultTangemSdkManager( cardId: String?, messageRes: Int?, allowsRequestAccessCodeFromRepository: Boolean, + shouldCheckIsAlreadyActivated: Boolean, ): CompletionResult { val message = Message(resources.getStringSafe(messageRes ?: R.string.initial_message_scan_header)) return coroutineScope { @@ -176,7 +177,7 @@ internal class DefaultTangemSdkManager( allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, visaCardScanHandler = visaCardScanHandler, visaCoroutineScope = this, - shouldCheckIsAlreadyActivated = true, + shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated, onboardingV2FeatureToggles = onboardingV2FeatureToggles, ), cardId = cardId, diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index 00e4ea971e..c70dc421f4 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -59,6 +59,7 @@ class MockTangemSdkManager( cardId: String?, messageRes: Int?, allowsRequestAccessCodeFromRepository: Boolean, + shouldCheckIsAlreadyActivated: Boolean, ): CompletionResult { return MockProvider.getScanResponse() } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index a45ca9387a..c75cbbbed9 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -296,7 +296,9 @@ internal class DefaultUserWalletsListRepository( } val scanResponse = unlockMethod.scanResponse ?: run { - when (val res = tangemSdkManagerProvider().scanProduct()) { + when (val res = tangemSdkManagerProvider().scanProduct( + shouldCheckIsAlreadyActivated = false, + )) { is CompletionResult.Failure -> raise(UnlockWalletError.UserCancelled) is CompletionResult.Success -> res.data } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt index 797ef6c056..1d9a6b2f6e 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 @@ -107,6 +107,7 @@ internal class CardSettingsModel @Inject constructor( private fun scanCard() = modelScope.launch { scanCardProcessor.scan( analyticsSource = com.tangem.core.analytics.models.AnalyticsParam.ScreensSources.Settings, + shouldCheckIsAlreadyActivated = false, allowsRequestAccessCodeFromRepository = true, ) .doOnSuccess { scanResponse -> diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt index f7a54bccda..bd97734029 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt @@ -11,10 +11,12 @@ interface ScanCardProcessor { cardId: String? = null, allowsRequestAccessCodeFromRepository: Boolean = false, analyticsSource: AnalyticsParam.ScreensSources, + shouldCheckIsAlreadyActivated: Boolean, ): CompletionResult suspend fun scan( analyticsSource: AnalyticsParam.ScreensSources, + shouldCheckIsAlreadyActivated: Boolean, cardId: String? = null, onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {}, onWalletNotCreated: suspend () -> Unit = {}, diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt index 04abfb1fce..046e484ec6 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt @@ -49,9 +49,14 @@ class ScanCardUseCase( cardId: String?, allowRequestAccessCodeFromStorage: Boolean, ): Either { - return Either.catch { scanCardRepository.scanCard(cardId, allowRequestAccessCodeFromStorage) } - .mapLeft { e -> - e as? ScanCardException ?: ScanCardException.UnknownException(e) - } + return Either.catch { + scanCardRepository.scanCard( + cardId = cardId, + allowRequestAccessCodeFromStorage = allowRequestAccessCodeFromStorage, + shouldCheckIsAlreadyActivated = true, // TODO use correct when enable NEW_CARD_SCANNING_ENABLED + ) + }.mapLeft { e -> + e as? ScanCardException ?: ScanCardException.UnknownException(e) + } } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt index 90f2016383..0774f11004 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt @@ -17,5 +17,9 @@ interface ScanCardRepository { * @return a [ScanResponse] object with the result of the scan. * @throws [ScanCardException] if the scan process fails. */ - suspend fun scanCard(cardId: String?, allowRequestAccessCodeFromStorage: Boolean): ScanResponse + suspend fun scanCard( + cardId: String?, + allowRequestAccessCodeFromStorage: Boolean, + shouldCheckIsAlreadyActivated: Boolean, + ): ScanResponse } \ No newline at end of file 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 6e840ea584..ac00b3703e 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 @@ -186,6 +186,7 @@ internal class CreateWalletStartModel @Inject constructor( scanCardProcessor.scan( analyticsSource = analyticsSource, + shouldCheckIsAlreadyActivated = true, onProgressStateChange = { showProgress -> if (!showProgress) { delay(HIDE_PROGRESS_DELAY) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt index 1db2f97cc0..3e81799932 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -120,6 +120,7 @@ internal class UserWalletSaver @Inject constructor( scope.launch { scanCardProcessor.scan( analyticsSource = AnalyticsParam.ScreensSources.Settings, + shouldCheckIsAlreadyActivated = true, onWalletNotCreated = { continuation.resume(Either.Right(null)) }, 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 767f2d00ec..7cb91f3880 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 @@ -171,6 +171,7 @@ internal class HomeModel @Inject constructor( scanCardProcessor.scan( analyticsSource = analyticsSource, + shouldCheckIsAlreadyActivated = true, onProgressStateChange = { showProgress -> if (!showProgress) { delay(HIDE_PROGRESS_DELAY) 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 5d32c8952b..49c8bc6351 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 @@ -104,6 +104,7 @@ internal class CreateHardwareWalletModel @Inject constructor( scanCardProcessor.scan( analyticsSource = analyticsSource, + shouldCheckIsAlreadyActivated = true, onProgressStateChange = { showProgress -> if (!showProgress) { delay(HIDE_PROGRESS_DELAY) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt index c7252d49c4..3d8d7bc0bd 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt @@ -115,7 +115,7 @@ internal class UpgradeWalletModel @Inject constructor( ) tangemSdkManager - .scanProduct() + .scanProduct(shouldCheckIsAlreadyActivated = true) .doOnSuccess { scanResponse -> checkIsWalletSuitableToBeUsedAsUpgrade(scanResponse = scanResponse) { delay(DELAY_SDK_DIALOG_CLOSE) diff --git a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt index 3aa198a9d7..5c16293dd7 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -192,6 +192,7 @@ internal class WelcomeModel @Inject constructor( modelScope.launch { scanCardProcessor.scan( analyticsSource = AnalyticsParam.ScreensSources.SignIn, + shouldCheckIsAlreadyActivated = true, onWalletNotCreated = {}, disclaimerWillShow = { router.pop() }, onSuccess = { scanResponse -> diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 423909e1bc..b1002f2997 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "releases-5.34-1430" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.34-581" +tangemCardSdk = "releases-5.34-584" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index 64016c2cf8..a7697062bd 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -45,10 +45,18 @@ interface TangemSdkManager { suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean = true): Boolean + /** + * Scans the card and returns [ScanResponse] with the card information. + * If [cardId] is provided, it will try to find the card with this ID in the card reader's range. + * If [allowsRequestAccessCodeFromRepository] is true, it will try to request access code from the repository + * if the card is protected by access code and the access code is not saved in the repository. + * If [shouldCheckIsAlreadyActivated] is true, it will check if the card is already activated and return an error if it is. + */ suspend fun scanProduct( cardId: String? = null, messageRes: Int? = null, allowsRequestAccessCodeFromRepository: Boolean = false, + shouldCheckIsAlreadyActivated: Boolean, ): CompletionResult suspend fun createProductWallet( From fa3b07b06688eb9373828dfb7a5d8aeac956643e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Feb 2026 23:06:21 +0700 Subject: [PATCH 094/106] Updated on 2026-08-14 --- .../feature/swap/DefaultSwapRepository.kt | 18 ------------- .../tangem/feature/swap/di/SwapDataModule.kt | 6 ----- .../feature/swap/domain/SwapInteractor.kt | 3 ++- .../feature/swap/domain/SwapInteractorImpl.kt | 25 +++++++++++-------- .../feature/swap/domain/api/SwapRepository.kt | 2 -- 5 files changed, 16 insertions(+), 38 deletions(-) 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 3146309d54..7e3ef9caeb 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 @@ -9,9 +9,7 @@ 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.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow @@ -25,7 +23,6 @@ import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.exchangeservice.swap.ExpressUtils import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.models.currency.CryptoCurrency @@ -53,19 +50,16 @@ internal class DefaultSwapRepository( private val tangemExpressApi: TangemExpressApi, private val coroutineDispatcher: CoroutineDispatcherProvider, private val walletManagersFacade: WalletManagersFacade, - private val userWalletsListRepository: UserWalletsListRepository, private val errorsDataConverter: ErrorsDataConverter, private val dataSignatureVerifier: DataSignatureVerifier, private val appPreferencesStore: AppPreferencesStore, private val rampStateManager: RampStateManager, moshi: Moshi, - excludedBlockchains: ExcludedBlockchains, ) : SwapRepository { private val expressDataConverter = ExpressDataConverter() private val leastTokenInfoConverter = LeastTokenInfoConverter() private val swapPairInfoConverter = SwapPairInfoConverter() - private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) private val exchangeStatusConverter = ExchangeStatusConverter() private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java) @@ -445,18 +439,6 @@ internal class DefaultSwapRepository( ) } - override suspend fun getNativeTokenForNetwork(networkId: String): CryptoCurrency { - val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - - return requireNotNull( - cryptoCurrencyFactory.createCoin( - blockchain = blockchain, - extraDerivationPath = null, - userWallet = requireNotNull(userWalletsListRepository.selectedUserWalletSync()), - ), - ) - } - private fun getDataError(ex: Exception): ExpressDataError { return if (ex is ApiResponseError.HttpException) { errorsDataConverter.convert(ex.errorBody.orEmpty()) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 91ff6e2f27..669ba6fcd4 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -1,7 +1,6 @@ package com.tangem.feature.swap.di import com.squareup.moshi.Moshi -import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.common.network.NetworkFactory import com.tangem.datasource.api.express.TangemExpressApi @@ -11,7 +10,6 @@ import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.SingleAccountListSupplier -import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.swap.DefaultSwapRepository @@ -37,10 +35,8 @@ internal class SwapDataModule { coroutineDispatcher: CoroutineDispatcherProvider, dataSignature: DataSignatureVerifier, walletManagerFacade: WalletManagersFacade, - userWalletsListRepository: UserWalletsListRepository, errorsDataConverter: ErrorsDataConverter, @NetworkMoshi moshi: Moshi, - excludedBlockchains: ExcludedBlockchains, appPreferencesStore: AppPreferencesStore, rampStateManager: RampStateManager, ): SwapRepository { @@ -48,11 +44,9 @@ internal class SwapDataModule { tangemExpressApi = tangemExpressApi, coroutineDispatcher = coroutineDispatcher, walletManagersFacade = walletManagerFacade, - userWalletsListRepository = userWalletsListRepository, errorsDataConverter = errorsDataConverter, dataSignatureVerifier = dataSignature, moshi = moshi, - excludedBlockchains = excludedBlockchains, appPreferencesStore = appPreferencesStore, rampStateManager = rampStateManager, ) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 244a784b00..c1193cdb4c 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -6,6 +6,7 @@ import com.tangem.domain.express.models.ExpressOperationType 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.UserWalletId import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.models.TransactionFeeExtended @@ -123,7 +124,7 @@ interface SwapInteractor { isReverseFromTo: Boolean, ): AccountSwapCurrency? - suspend fun getNativeToken(networkId: String): CryptoCurrency + suspend fun getNativeToken(network: Network): CryptoCurrency @Suppress("LongParameterList") suspend fun storeSwapTransaction( 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 cbaa2d6509..c3793db861 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 @@ -1355,8 +1355,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( } } - override suspend fun getNativeToken(networkId: String): CryptoCurrency { - return repository.getNativeTokenForNetwork(networkId) + override suspend fun getNativeToken(network: Network): CryptoCurrency { + return multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + ?.filterIsInstance() + ?.firstOrNull { it.network.id == network.id && it.network.derivationPath == network.derivationPath } + ?: error("Unable to create network coin with ID: ${network.id}") } private suspend fun isAllowedToSpend( @@ -1505,7 +1510,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( return quoteDataModel.fold( ifRight = { quoteModel -> val swapState = updateBalances( - networkId = networkId, fromTokenStatus = fromToken, fromAccount = fromAccount, toTokenStatus = toToken, @@ -1753,10 +1757,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( } else -> { if (feeValue < amount.value) { + val nativeCoinDecimals = Blockchain.fromNetworkId(networkId)?.decimals() + ?: error("Blockchain not found") IncludeFeeInAmount.Included( amountSubtractFee = SwapAmount( reducedBalance - feeValue, - getNativeToken(fromToken.network.backendId).decimals, + nativeCoinDecimals, ), ) } else { @@ -1773,7 +1779,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) val feeCurrencyId: CryptoCurrency.ID = when (feePaidCurrency) { is FeePaidCurrency.Token -> feePaidCurrency.tokenId - else -> getNativeToken(networkId = fromToken.network.backendId).id + else -> getNativeToken(network = fromToken.network).id } val rates = getQuotes(feeCurrencyId) return rates[feeCurrencyId]?.let { rate -> @@ -1869,7 +1875,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( includeFeeInAmount = includeFeeInAmount, ) val swapState = updateBalances( - networkId = networkId, fromTokenStatus = fromToken, fromAccount = fromAccount, toTokenStatus = toToken, @@ -2037,7 +2042,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( @Suppress("LongParameterList", "MaxChainedCallsOnSameLine") private suspend fun updateBalances( provider: SwapProvider, - networkId: String, fromTokenStatus: CryptoCurrencyStatus, fromAccount: Account.CryptoPortfolio?, toTokenStatus: CryptoCurrencyStatus, @@ -2049,7 +2053,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( ): SwapState.QuotesLoadedState { val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency - val nativeToken = repository.getNativeTokenForNetwork(networkId) + val nativeToken = getNativeToken(fromToken.network) val rates = getQuotes(fromToken.id, toToken.id, nativeToken.id) return SwapState.QuotesLoadedState( fromTokenInfo = TokenSwapInfo( @@ -2478,7 +2482,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( if (balanceToCheck > fee.multiply(percentsToFeeIncrease)) { SwapFeeState.Enough } else { - val nativeToken = getNativeToken(fromTokenStatus.currency.network.backendId) + val nativeToken = getNativeToken(fromTokenStatus.currency.network) SwapFeeState.NotEnough( feeCurrency = nativeToken, currencyName = nativeToken.network.name, @@ -2522,11 +2526,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( } } is FeePaidCurrency.FeeResource -> { - val network = repository.getNativeTokenForNetwork(networkId).network val isFeeResourceEnough = currencyChecksRepository.checkIfFeeResourceEnough( amount = spendAmount.value, userWalletId = userWalletId, - network = network, + network = fromTokenStatus.currency.network, ) if (isFeeResourceEnough) { 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 a23260fb53..c9ba429d75 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 @@ -83,6 +83,4 @@ interface SwapRepository { txHash: String, payInExtraId: String?, ): Either - - suspend fun getNativeTokenForNetwork(networkId: String): CryptoCurrency } \ No newline at end of file From 4281d5da0650517207551fcd29004ae18365501a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 13:19:46 +0500 Subject: [PATCH 095/106] Updated on 2026-08-14 --- .../wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt index b146be18d6..a9ef754830 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt @@ -2,7 +2,7 @@ package com.tangem.domain.wallets.usecase import com.tangem.common.TangemSiteUrlBuilder import com.tangem.domain.wallets.repository.WalletsPromoRepository -import java.util.Locale +import com.tangem.utils.SupportedLanguages class GenerateBuyTangemCardLinkUseCase( private val walletsPromoRepository: WalletsPromoRepository, @@ -15,7 +15,7 @@ class GenerateBuyTangemCardLinkUseCase( suspend operator fun invoke(utmCampaign: String?): String { val refCode = walletsPromoRepository.getReferralCodeIfExists() val refCodeTag = TangemSiteUrlBuilder.getRefCodeTag(refCode) - val langCode = Locale.getDefault().language + val langCode = SupportedLanguages.getCurrentSupportedLanguageCode() val utmTags = TangemSiteUrlBuilder.getUtmTags(utmCampaign) return "https://buy.tangem.com/$langCode?$utmTags&$refCodeTag" } From 18ef37d290ecd92d81bb16dd6c79ddf952638d98 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 11:46:57 +0300 Subject: [PATCH 096/106] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 4 ++++ core/res/src/main/res/values-es/strings.xml | 2 ++ core/res/src/main/res/values-fr/strings.xml | 4 ++++ core/res/src/main/res/values-ru/strings.xml | 2 ++ core/res/src/main/res/values-uk-rUA/strings.xml | 2 ++ core/res/src/main/res/values/strings.xml | 2 +- 6 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 9903332ae6..4cc165f633 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1431,6 +1431,8 @@ Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen. Tausche mehr Token zu besseren Kursen direkt in deiner Brieftasche. Neuer Swap-Anbieter verfügbar! + Suchen Sie nach einem beliebigen Token, auch wenn es noch nicht in Ihrer Liste ist. + Nutzen Sie die Suche, um zu finden, was Sie benötigen. Vertraue auf den rund um die Uhr verfügbaren Support bei allen Problemen Immer für Dich da Mehrere vertrauenswürdige Anbieter an einem Ort – tausche mühelos alle Vermögenswerte in Deiner Wallet @@ -1888,6 +1890,8 @@ Service vorübergehend nicht verfügbar Die Menge der zu tauschenden Token darf folgende Werte nicht überschreiten %s Der zu tauschende Betrag muss mindestens %s betragen + Der Tausch ist für dieses Paar nicht verfügbar. Bitte wählen Sie ein anderes Token und versuchen Sie es erneut. + Nicht unterstütztes Swap-Paar. Bitte änder den zu tauschenden Betrag Bei dieser Karte oder Ring kann es sich um ein Produktionsmuster oder eine Fälschung handeln Echtheitsprüfung fehlgeschlagen diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 462fd75c00..0ec38e3506 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1896,6 +1896,8 @@ Servicio no disponible temporalmente La cantidad de tokens a intercambiar no debe exceder %s El monto a cambiar debe ser de al menos %s + El intercambio no está disponible para este par. Por favor, seleccione otro token e inténtelo de nuevo. + Par de intercambio no compatible. Por favor cambie la cantidad a cambiar Esta tarjeta podría ser una muestra de producción o una falsificación La verificación de autenticidad falló diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index fcdf84f726..41913d6515 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1434,6 +1434,8 @@ Le réseau facturera des frais d\'approbation de jeton pour vérifier que vous autorisez l\'utilisation de votre jeton pour l\'échange. Échangez plus de jetons à de meilleurs taux directement dans votre portefeuille. Nouveau fournisseur d\'échange disponible ! + Recherchez n’importe quel token, même s’il ne figure pas encore dans votre liste. + Utilisez la recherche pour trouver ce dont vous avez besoin. Ayez confiance en notre assistance 24 heures sur 24 pour vous aider à résoudre tous vos problèmes Assistance 24 heures sur 24 Plusieurs fournisseurs de confiance en un seul endroit : échangez n\'importe quel actif facilement @@ -1891,6 +1893,8 @@ Service temporairement indisponible Le nombre de jetons à échanger ne doit pas dépasser %s Le montant à échanger doit être d\'au moins %s + L’échange n’est pas disponible pour cette paire. Veuillez sélectionner un autre token et réessayer. + Paire d’échange non prise en charge. Veuillez modifier le montant à échanger Cette carte pourrait être un échantillon de production ou une contrefaçon Échec de la vérification d\'authenticité diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 5a0683d679..350f3c2c43 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1864,6 +1864,8 @@ Сервис временно недоступен Сумма для обмена должна быть не более %s Сумма для обмена должна быть не менее %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 94cc71f5db..297d8310dc 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1894,6 +1894,8 @@ Сервіс тимчасово недоступний Сума до обміну не повинна перевищувати %s Сума для обміну має бути не менше %s + Обмін для цієї пари недоступний. Будь ласка, виберіть інший токен і спробуйте знову. + Непідтримувана пара для обміну. Будь ласка, змініть суму для обміну Ця картка може бути виробничим зразком або підробкою Перевірка автентичності не вдалася diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index dbc8f644d4..f40c33cb35 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1898,7 +1898,7 @@ The amount of tokens to be swapped must not exceed %s The amount to swap must be at least %s Swapping isn\'t available for this pair. Please select a different token and try again. - Unsupported token pair + Unsupported swap pair Please change the amount to swap This card might be a production sample or counterfeit Authenticity check failed From e18ac0b8fab300d6f4b272f1e8d90e79d579d8a7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 12:56:50 +0400 Subject: [PATCH 097/106] Updated on 2026-08-14 --- .../data/swap/DefaultSwapRepositoryV2.kt | 75 +++++++++++-------- 1 file changed, 43 insertions(+), 32 deletions(-) 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 080f06bacf..0386fa396c 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 @@ -4,6 +4,8 @@ import arrow.core.none import arrow.core.some import arrow.core.toOption import com.squareup.moshi.Moshi +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.api.safeApiCall import com.tangem.data.swap.converter.SwapDataConverter import com.tangem.data.swap.converter.SwapStatusConverter @@ -132,42 +134,43 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( swapTxType = swapTxType, ).associateBy(ExpressProvider::providerId) - allPairs.map { pair -> - async { - val statusFromDeferred = async { - cryptoCurrencyList - .firstOrNull { currency -> - currency.getContractAddress() == pair.from.contractAddress && - currency.network.backendId == pair.from.network - } - } - val statusToDeferred = async { - cryptoCurrencyList - .firstOrNull { currency -> - currency.getContractAddress() == pair.to.contractAddress && - currency.network.backendId == pair.to.network - } - } + allPairs.filter { it.to.network !in MEMO_RESTRICTED_NETWORKS } + .map { pair -> + async { + val statusFromDeferred = async { + cryptoCurrencyList + .firstOrNull { currency -> + currency.getContractAddress() == pair.from.contractAddress && + currency.network.backendId == pair.from.network + } + } + val statusToDeferred = async { + cryptoCurrencyList + .firstOrNull { currency -> + currency.getContractAddress() == pair.to.contractAddress && + currency.network.backendId == pair.to.network + } + } - val currencyStatusFrom = createSendWithSwapCryptoCurrencyStatus(statusFromDeferred.await()) - val currencyStatusTo = createSendWithSwapCryptoCurrencyStatus(statusToDeferred.await()) + val currencyStatusFrom = createSendWithSwapCryptoCurrencyStatus(statusFromDeferred.await()) + val currencyStatusTo = createSendWithSwapCryptoCurrencyStatus(statusToDeferred.await()) - val mappedProvider = pair.providers - .filterNot { it.hasOnlyFixedRateType() } - .mapNotNull { mappedProviders[it.providerId] } - .filterYieldSupplyProvider(currencyStatusFrom) + val mappedProvider = pair.providers + .filterNot { it.hasOnlyFixedRateType() } + .mapNotNull { mappedProviders[it.providerId] } + .filterYieldSupplyProvider(currencyStatusFrom) - if (currencyStatusFrom != null && currencyStatusTo != null && mappedProvider.isNotEmpty()) { - SwapPairModel( - from = currencyStatusFrom, - to = currencyStatusTo, - providers = mappedProvider, - ) - } else { - null + if (currencyStatusFrom != null && currencyStatusTo != null && mappedProvider.isNotEmpty()) { + SwapPairModel( + from = currencyStatusFrom, + to = currencyStatusTo, + providers = mappedProvider, + ) + } else { + null + } } - } - }.awaitAll().filterNotNull() + }.awaitAll().filterNotNull() } override suspend fun getSwapQuote( @@ -440,6 +443,14 @@ 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(), +) + private suspend fun ExpressRepository.getFilteredProviders( userWallet: UserWallet, filterProviderTypes: List, From 9359c75f42eeaab1fc3a2091f4fd895de985de88 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 12:45:55 +0300 Subject: [PATCH 098/106] Updated on 2026-08-14 --- .../common/TangemSiteShareUrlBuilder.kt | 31 +++++++++++++++++++ .../details/MarketsTokenDetailsModel.kt | 4 +-- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt diff --git a/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt new file mode 100644 index 0000000000..64554a30e0 --- /dev/null +++ b/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt @@ -0,0 +1,31 @@ +package com.tangem.common + +import com.tangem.utils.SupportedLanguages.CHINESE +import com.tangem.utils.SupportedLanguages.ENGLISH +import com.tangem.utils.SupportedLanguages.FRANCH +import com.tangem.utils.SupportedLanguages.GERMAN +import com.tangem.utils.SupportedLanguages.JAPANESE +import java.util.Locale + +object TangemSiteShareUrlBuilder { + + private const val BASE_URL = "https://tangem.com" + private const val CRYPTOCURRENCIES_PATH = "cryptocurrencies" + + @Deprecated("Should use CHINESE from SupportedLanguages, but the site expects zh-Hans in the URL path") + private const val CHINESE_SITE_LOCALE = "zh-Hans" + + @Deprecated("Should rely on SupportedLanguages instead of maintaining a separate list") + private val siteLocales = mapOf( + ENGLISH to ENGLISH, + FRANCH to FRANCH, + GERMAN to GERMAN, + JAPANESE to JAPANESE, + CHINESE to CHINESE_SITE_LOCALE, + ) + + fun shareUrl(tokenId: String): String { + val locale = siteLocales[Locale.getDefault().language] ?: ENGLISH + return "$BASE_URL/$locale/$CRYPTOCURRENCIES_PATH/$tokenId" + } +} \ 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 257c93ed3b..e4af70344b 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 @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import arrow.core.Either import arrow.core.getOrElse import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.common.TangemSiteShareUrlBuilder import com.tangem.common.ui.charts.state.MarketChartData import com.tangem.common.ui.charts.state.MarketChartDataProducer import com.tangem.common.ui.charts.state.sorted @@ -667,7 +668,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( private fun onShareClick() { val tokenId = params.token.id.value - val shareUrl = "$CRYPTOCURRENCIES_BASE_URL$tokenId" + val shareUrl = TangemSiteShareUrlBuilder.shareUrl(tokenId) shareManager.shareText(shareUrl) analyticsEventHandler.send(analyticsEventBuilder.shareClicked()) } @@ -735,6 +736,5 @@ internal class MarketsTokenDetailsModel @Inject constructor( private companion object { const val QUOTES_UPDATE_INTERVAL_MILLIS = 60000L const val RELATED_NEWS_LIMIT = 10 - const val CRYPTOCURRENCIES_BASE_URL = "https://tangem.com/en/cryptocurrencies/" } } \ No newline at end of file From a6a5d5e1452a8f266983189e4ee1fc58fe166702 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 15:14:05 +0500 Subject: [PATCH 099/106] Updated on 2026-08-14 --- .../SwapTransactionErrorStateConverter.kt | 6 +++--- .../tangem/feature/swap/model/SwapModel.kt | 20 +++++++++++-------- .../tangem/feature/swap/models/SwapAlertUM.kt | 11 +++++++++- .../tangem/feature/swap/ui/StateBuilder.kt | 20 +++++++++++++++++-- 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt index 646e3a3218..dc6c9cfbb0 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt @@ -17,7 +17,7 @@ internal class SwapTransactionErrorStateConverter( is SwapTransactionState.Error.TransactionError -> { when (val error = value.error) { is SendTransactionError.UserCancelledError -> return null - null -> SwapAlertUM.GenericError(onDismiss) + null -> SwapAlertUM.DefaultError(onDismiss) else -> TransactionErrorAlertConverter(onDismiss, onSupportClick).convert(error) } } @@ -27,8 +27,8 @@ internal class SwapTransactionErrorStateConverter( onConfirmClick = { onSupportClick(value.error.code.toString()) }, ) } - SwapTransactionState.Error.UnknownError -> SwapAlertUM.GenericError(onDismiss) - is SwapTransactionState.Error.TangemPayWithdrawalError -> SwapAlertUM.GenericError( + SwapTransactionState.Error.UnknownError -> SwapAlertUM.DefaultError(onDismiss) + is SwapTransactionState.Error.TangemPayWithdrawalError -> SwapAlertUM.SupportError( onConfirmClick = { onSupportClick(value.txId) }, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 2727fa50c7..64a9dc67b1 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 @@ -342,7 +342,7 @@ internal class SwapModel @Inject constructor( } if (fromAccountStatus == null) { - uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back) + uiState = stateBuilder.addDefaultAlert(uiState = uiState, onDismiss = swapRouter::back) } else { fromAccountCurrencyStatus = fromAccountStatus toAccountCurrencyStatus = toAccountStatus @@ -360,7 +360,7 @@ internal class SwapModel @Inject constructor( } if (fromStatus == null) { - uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back) + uiState = stateBuilder.addDefaultAlert(uiState = uiState, onDismiss = swapRouter::back) } else { initialFromStatus = fromStatus initialToStatus = toStatus @@ -1020,7 +1020,7 @@ internal class SwapModel @Inject constructor( val fee = getSelectedFee() if (fee == null && tangemPayInput?.isWithdrawal != true) { - makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + makeSupportAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) modelScope.launch { delay(SWAP_IN_PROGRESS_DELAY) startLoadingQuotesFromLastState() @@ -1046,7 +1046,7 @@ internal class SwapModel @Inject constructor( when (swapTransactionState) { is SwapTransactionState.TxSent -> { if (fee == null) { - makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + makeSupportAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) return@onSuccess } sendSuccessSwapEvent( @@ -1205,7 +1205,7 @@ internal class SwapModel @Inject constructor( } val feeForPermission = when (val fee = approveDataModel.fee) { TxFeeState.Empty -> { - makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + makeSupportAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) Timber.e("Fee should not be Empty") return@launch } @@ -1658,11 +1658,15 @@ internal class SwapModel @Inject constructor( } private fun makeDefaultAlert() { - uiState = stateBuilder.addAlert(uiState) + uiState = stateBuilder.addDefaultAlert(uiState = uiState) } - private fun makeDefaultAlert(message: TextReference) { - uiState = stateBuilder.addAlert(uiState, message) + private fun makeSupportAlert(message: TextReference) { + uiState = stateBuilder.addSupportAlert( + uiState = uiState, + message = message, + onSupportClick = { onFailedTxEmailClick("Fee calculation error") }, + ) } @Suppress("LongMethod", "CyclomaticComplexMethod") diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt index bb84996732..bb7763f928 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt @@ -7,7 +7,16 @@ import com.tangem.core.ui.extensions.resourceReference sealed class SwapAlertUM : AlertUM { - data class GenericError( + data class DefaultError( + override val onConfirmClick: (() -> Unit), + override val message: TextReference = resourceReference(R.string.common_unknown_error), + ) : SwapAlertUM() { + override val title: TextReference? = null + override val confirmButtonText: TextReference = + resourceReference(id = R.string.common_ok) + } + + data class SupportError( override val onConfirmClick: (() -> Unit), override val message: TextReference = resourceReference(R.string.common_unknown_error), ) : SwapAlertUM() { 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 52bc5915e7..6e0f5f520a 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 @@ -1069,7 +1069,7 @@ internal class StateBuilder( ) } - fun addAlert( + fun addDefaultAlert( uiState: SwapStateHolder, message: TextReference = resourceReference(R.string.common_unknown_error), onDismiss: () -> Unit = { clearAlert(uiState) }, @@ -1077,7 +1077,23 @@ internal class StateBuilder( return uiState.copy( event = triggeredEvent( SwapEvent.ShowAlert( - SwapAlertUM.GenericError(onDismiss, message), + SwapAlertUM.DefaultError(onDismiss, message), + ), + onConsume = onDismiss, + ), + ) + } + + fun addSupportAlert( + uiState: SwapStateHolder, + message: TextReference = resourceReference(R.string.common_unknown_error), + onDismiss: () -> Unit = { clearAlert(uiState) }, + onSupportClick: () -> Unit, + ): SwapStateHolder { + return uiState.copy( + event = triggeredEvent( + SwapEvent.ShowAlert( + SwapAlertUM.SupportError(onSupportClick, message), ), onConsume = onDismiss, ), From 76b718672c80a93d2a2f48e73f38f5028c5528b2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 13:22:15 +0300 Subject: [PATCH 100/106] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 97a76b6ac8..2ef3c24014 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -313,6 +313,16 @@ + + + + + + + + Date: Fri, 27 Feb 2026 14:14:36 +0300 Subject: [PATCH 101/106] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index b1002f2997..a3bc5cab8e 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "releases-5.34-1430" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.34-584" +tangemCardSdk = "releases-5.34-587" #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 720759fb1ac94c75da527a656b2cf87237f98376 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 17:40:42 +0500 Subject: [PATCH 102/106] Updated on 2026-08-14 --- .../send/v2/common/SendConfirmAlertFactory.kt | 11 +++++++---- .../supply/impl/common/YieldSupplyAlertFactory.kt | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt index 78b805b69a..d066f0da3a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt @@ -22,10 +22,13 @@ internal class SendConfirmAlertFactory @Inject constructor( title = resourceReference(id = R.string.send_alert_transaction_failed_title), message = resourceReference(id = R.string.common_unknown_error), onDismissRequest = popBack, - firstAction = EventMessageAction( - title = resourceReference(R.string.common_support), - onClick = onFailedTxEmailClick, - ), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_support), + onClick = onFailedTxEmailClick, + ) + }, + secondActionBuilder = { cancelAction { } }, ), ) } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyAlertFactory.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyAlertFactory.kt index 00958d2ec1..e5f5e68cca 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyAlertFactory.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyAlertFactory.kt @@ -32,10 +32,13 @@ class YieldSupplyAlertFactory @Inject constructor( title = resourceReference(id = R.string.send_alert_transaction_failed_title), message = resourceReference(id = R.string.common_unknown_error), onDismissRequest = popBack, - firstAction = EventMessageAction( - title = resourceReference(R.string.common_support), - onClick = onFailedTxEmailClick, - ), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_support), + onClick = onFailedTxEmailClick, + ) + }, + secondActionBuilder = { cancelAction { } }, ), ) } From 5d204c2bf09bcead01a767c57b00cf58941bf47f Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 19:46:42 +0700 Subject: [PATCH 103/106] Updated on 2026-08-14 --- .../src/main/java/com/tangem/feature/swap/model/SwapModel.kt | 2 ++ 1 file changed, 2 insertions(+) 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 64a9dc67b1..ab7b88c30d 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 @@ -2397,6 +2397,8 @@ internal class SwapModel @Inject constructor( } override fun onResult(newState: FeeSelectorUM) { + state.value = newState + if (newState is FeeSelectorUM.Error) { modelScope.launch { forceUpdateState.emit(newState.copy(isHidden = true)) From 6a04ccb2850c727eb2ad2b660efe76599cfa9f0b Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 1 Mar 2026 13:55:27 +0400 Subject: [PATCH 104/106] Updated on 2026-08-14 --- .../core/ui/security/DisableAutofill.kt | 31 +++++++++++++++++++ .../ui/MultiWalletSeedPhraseImport.kt | 3 ++ 2 files changed, 34 insertions(+) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/security/DisableAutofill.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/security/DisableAutofill.kt b/core/ui/src/main/java/com/tangem/core/ui/security/DisableAutofill.kt new file mode 100644 index 0000000000..8693a0ea3e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/security/DisableAutofill.kt @@ -0,0 +1,31 @@ +package com.tangem.core.ui.security + +import android.os.Build +import android.view.View +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.platform.LocalView + +/** + * Disables autofill for the current Compose view hierarchy. + * + * This prevents password managers and other autofill services from accessing + * sensitive content (e.g., seed phrases, private keys) entered in text fields. + * + * Must be called within a Composable scope before the text fields that need protection. + * + * The original autofill setting is restored when this composable leaves the composition. + */ +@Composable +fun DisableAutofillEffect() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val view = LocalView.current + DisposableEffect(view) { + val previousValue = view.importantForAutofill + view.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS + onDispose { + view.importantForAutofill = previousValue + } + } + } +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt index 8779ab378a..a5fda3ab83 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt @@ -28,6 +28,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.security.DisableAutofillEffect import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.utils.InvalidWordsColorTransformation @@ -53,6 +54,8 @@ internal fun MultiWalletSeedPhraseImport(state: MultiWalletSeedPhraseUM.Import, ) } + DisableAutofillEffect() + Box(modifier.fillMaxSize()) { Column( modifier = Modifier From cf94c1f82622a60d68b3933edd47b55baf77ac1e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Mar 2026 19:15:58 +0500 Subject: [PATCH 105/106] Updated on 2026-08-14 --- .../usecase/ManageCryptoCurrenciesUseCase.kt | 17 ++++++-- .../model/CustomTokenFormModel.kt | 41 +++++++++++-------- .../list/CustomTokenFormUseCasesFacade.kt | 6 ++- 3 files changed, 42 insertions(+), 22 deletions(-) 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 2bef64e9b1..2774fa49fe 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 @@ -84,7 +84,7 @@ class ManageCryptoCurrenciesUseCase( withContext(NonCancellable) { val accountStatus = getAccountStatus(accountId = accountId) - val modifiedCurrencyList = accountStatus.tokenList.flattenCurrencies() + var modifiedCurrencyList = accountStatus.tokenList.flattenCurrencies() .modify(add = add, remove = remove) if (!modifiedCurrencyList.hasChanges) { @@ -92,12 +92,21 @@ class ManageCryptoCurrenciesUseCase( return@withContext } + val derivingResult = derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) + + if (!skipDerivationErrors && derivingResult.isLeft()) { + modifiedCurrencyList = accountStatus.tokenList.flattenCurrencies() + .modify(add = emptyList(), remove = remove) + + if (!modifiedCurrencyList.hasChanges) { + derivingResult.bind() + } + } + saveAccount( account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total), ) - val result = derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) - parallelUpdatingScope.launch { syncTokens(userWalletId, modifiedCurrencyList) @@ -107,7 +116,7 @@ class ManageCryptoCurrenciesUseCase( } if (!skipDerivationErrors) { - result.bind() + derivingResult.bind() } } } 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 1ce00a7b83..d741fc5ed0 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.common.core.TangemSdkError import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -8,6 +9,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase import com.tangem.domain.managetokens.FindTokenUseCase @@ -146,7 +148,7 @@ internal class CustomTokenFormModel @Inject constructor( is CustomCurrencyValidator.Status.Validating, -> Unit is CustomCurrencyValidator.Status.SearchingToken -> updateStateWithProgress() - is CustomCurrencyValidator.Status.UnexpectedException -> showErrorDialog() + is CustomCurrencyValidator.Status.UnexpectedException -> showErrorDialog(validatorState.cause) is CustomCurrencyValidator.Status.FormValidationException -> updateStateWithExceptions( exceptions = validatorState.exceptions, ) @@ -236,9 +238,17 @@ internal class CustomTokenFormModel @Inject constructor( } } - private fun showErrorDialog() { + private fun showErrorDialog(throwable: Throwable) { + Timber.e(throwable) + val message = when (throwable) { + is TangemSdkError -> resourceReference( + R.string.generic_error_code, + wrappedList(throwable.code.toString()), + ) + else -> resourceReference(R.string.common_unknown_error) + } val dialog = DialogMessage( - message = resourceReference(R.string.common_unknown_error), + message = message, ) messageSender.send(dialog) @@ -341,8 +351,17 @@ internal class CustomTokenFormModel @Inject constructor( ) { val currency = createdCurrency if (currency == null) { - Timber.e("Trying to add currency without validation") - showErrorDialog() + showErrorDialog(IllegalStateException("Trying to add currency without validation")) + return@resource + } + + useCasesFacade.derivePublicKeysUseCase(listOf(currency)).getOrElse { + showErrorDialog(IllegalStateException("Failed to derive public keys")) + return@resource + } + + useCasesFacade.addCryptoCurrenciesUseCase(currency).getOrElse { throwable -> + showErrorDialog(throwable) return@resource } @@ -353,18 +372,6 @@ internal class CustomTokenFormModel @Inject constructor( ) analyticsEventHandler.send(event) - useCasesFacade.derivePublicKeysUseCase(listOf(currency)).getOrElse { - Timber.e(it, "Failed to derive public keys") - showErrorDialog() - return@resource - } - - useCasesFacade.addCryptoCurrenciesUseCase(currency).getOrElse { - Timber.e(it, "Failed to add currency") - showErrorDialog() - return@resource - } - params.onCurrencyAdded(currency) } 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 51a46bd0e0..d446dd0141 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 @@ -44,7 +44,11 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( either { val accountId = getAccountId(currency) - manageCryptoCurrenciesUseCase(accountId = accountId, add = currency).bind() + manageCryptoCurrenciesUseCase( + accountId = accountId, + add = currency, + skipDerivationErrors = false, + ).bind() } } else { addCryptoCurrenciesUseCase.invoke(userWalletId = userWalletId, currency = currency) From 6f1c50c2a538133d40356fd4db1af25fd483fefd Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Mar 2026 18:29:38 +0200 Subject: [PATCH 106/106] 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 e4fac168bd..b693d601d9 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit e4fac168bd941fe90b0f081dfa70ea1b4148b49f +Subproject commit b693d601d9dc3de27f0536574bce59a6aa5e2e89