From 6b05c5566ebc497a29b02f0c7f77619e059e74b1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Feb 2026 15:15:31 +0300 Subject: [PATCH 01/27] 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 02/27] 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 03/27] 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 04/27] 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 05/27] 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 06/27] 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 07/27] 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 08/27] 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 09/27] 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 10/27] 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 11/27] 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 12/27] 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 13/27] 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 14/27] 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 27921d8c3afe2a3e60ca4375a4c0b00009f1a501 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Feb 2026 15:13:10 +0100 Subject: [PATCH 15/27] 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 16/27] 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 17/27] 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 18/27] 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 19/27] 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 20/27] 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 21/27] 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 22/27] 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 23/27] 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 24/27] 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 25/27] 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 26/27] 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 f6d61b2b7f25be6365fbb62d662bcf56fd03c0c9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 9 Feb 2026 15:40:17 +0400 Subject: [PATCH 27/27] 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), )