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/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 009af28e09..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 -> @@ -147,7 +149,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/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/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/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/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/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..ff3a31ce16 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MarketsPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MarketsPageObject.kt @@ -1,57 +1,44 @@ 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) useUnmergedTree = true } + val searchThroughMarketPlaceholder: KNode = child { + hasText(getResourceString(R.string.markets_search_header_title)) + useUnmergedTree = true + } + 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/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/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/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/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/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/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/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..2f3f1a81d6 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapStoriesTest.kt @@ -0,0 +1,437 @@ +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.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 +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("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.MainScreen) + } + 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("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + 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' 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("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.MarketsTokenDetails) + } + step("Click on 'Close' button") { + onSwapTokenScreen { closeButton.performClick() } + } + step("Assert 'Swap' button has not badge") { + onMarketsTokenDetailsScreen { swapPortfolioQuickActionButton.assertContentDescriptionEquals(badgeHidden) } + } + } + } + + @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/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/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/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/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/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/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/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 53c7fa8abd..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 @@ -13,7 +13,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.* @@ -22,7 +21,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 @@ -38,105 +36,61 @@ 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, analyticsEventHandler: AnalyticsEventHandler, ): SaveWalletUseCase { return SaveWalletUseCase( - userWalletsListManager = userWalletsListManager, userWalletsListRepository = userWalletsListRepository, walletsRepository = walletsRepository, - useNewRepository = hotWalletFeatureToggles.isHotWalletEnabled, analyticsEventHandler = analyticsEventHandler, ) } @@ -144,15 +98,9 @@ internal object WalletsDomainModule { @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 @@ -167,12 +115,6 @@ internal object WalletsDomainModule { return GetExploreUrlUseCase(walletsManagersFacade = walletsManagersFacade) } - @Provides - @Singleton - fun providesUnlockWalletsUseCase(userWalletsListManager: UserWalletsListManager): UnlockWalletsUseCase { - return UnlockWalletsUseCase(userWalletsListManager = userWalletsListManager) - } - @Provides @Singleton fun providesUnlockWalletUseCase( @@ -200,31 +142,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 @@ -241,44 +171,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 @@ -350,15 +250,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 @@ -482,15 +376,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, ) } @@ -508,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/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index 92f1c4702e..1fab5009e0 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,20 +16,15 @@ 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.feature.referral.domain.MobileWalletPromoRepository 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 @@ -48,77 +43,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( @@ -186,7 +110,7 @@ internal object UserWalletsListManagerModule { ) } - fun buildMoshi(): Moshi { + private fun buildMoshi(): Moshi { return Moshi.Builder() .add(WalletDerivedKeysMapAdapter()) .add(ScanResponseDerivedKeysMapAdapter()) @@ -203,7 +127,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/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index 8471ade6de..d9541b317a 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.* @@ -30,19 +31,20 @@ 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.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 +import kotlinx.coroutines.withContext @Suppress("LongParameterList", "LargeClass") internal class DefaultUserWalletsListRepository( @@ -64,8 +66,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 @@ -100,6 +111,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) { @@ -295,8 +313,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 } @@ -395,6 +412,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, @@ -435,9 +490,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/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/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/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/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/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/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 3000c53538..301a49d9ba 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, @@ -260,16 +258,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 95d4585b71..aa9b5efd77 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, ) { @@ -163,21 +160,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/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/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/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/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/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 208a5cf457..399317f1ee 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" @@ -86,5 +78,9 @@ { "name": "HOLD_TO_CONFIRM_BUTTON_ENABLED", "version": "undefined" + }, + { + "name": "WALLET_REORDER_FEATURE_ENABLED", + "version": "undefined" } ] 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 f9bdbc2aa6..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 @@ -205,4 +205,18 @@ interface TangemTechApi { */ @POST("v1/transaction-events") suspend fun transactionEvents(@Body name: TransactionEventBody): ApiResponse + + // region Earn + @GET("v1/earn/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..47ec19a2ff --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt @@ -0,0 +1,34 @@ +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, + @Json(name = "decimalCount") val decimalCount: Int? = null, +) + +@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/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 bf9d444051..fab336d555 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 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/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index d554a4bd2f..46949f43d3 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -86,7 +86,7 @@ このアドレスには%3$s ネットワークから%1$s (%2$s) のみを送信してください。他のトークンやネットワークを使用すると、資金を失う可能性があります。 デフォルト レガシー - デバイスの生体認証をリセットするか、サポートにお問い合わせください + 生体認証で問題が発生しました。端末の生体認証をリセットするか、サポートにお問い合わせください。 認証エラー スキャン方法 サポートをリクエストする @@ -461,9 +461,15 @@ %sネットワーク 下記のみを使用して資金を送金する おすすめ + 絞り込みを解除 + リストは現在更新中のため、一時的に空になっています。しばらくしてからご確認ください。 すべてのネットワーク すべての種類 + 絞り込み + マイネットワーク + ネットワーク よく使われています + 該当する結果はありません 運用 こんにちは、サポートチームの皆さん、コード %s のエラーが発生しました。 WalletConnectエラー @@ -814,6 +820,7 @@ 取引量 これをドラッグするか、検索窓をタップして、マーケットから直接トークンを追加します トークンを追加 + トークンを追加 資産を常時アクセス可能な状態に保ったまま、パワーアップさせよう。%s 利息モードを有効にする モバイルウォレットを作成するには、%1$sにアップデートする必要があります @@ -827,7 +834,7 @@ %d分前 クイックまとめ - 関連ニュース + ニュース 関連トークン 関連ニュース 最新情報を入手 diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index cd70c14fcd..fa1ef254d0 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -311,6 +311,7 @@ Перейти до токену Зrozуміло Приховати + Утримуйте, щоб %s година Імпортувати В процесі @@ -337,10 +338,12 @@ Ні Немає адреси Не додано + Недоступно Не зараз Зараз ОК Відкрити в браузері + або Основна картка Основне кільце Парольна фраза @@ -386,6 +389,7 @@ Обмін Tangem Tangem Wallet + Натисніть і утримуйте умови участі Умовами використання До @@ -453,6 +457,7 @@ Цей механізм захищає картку або кільце від безконтактних атак. Між скануванням картки та виконанням команди буде додана затримка. Пароль Перед виконанням будь-якої команди, що тягне за собою зміну стану картки, вам необхідно буде ввести пароль. + Оновлення до апаратного гаманця NFT Реферальна програма Переверніть екран пристрою вниз, щоб швидко приховати та відобразити баланси @@ -466,6 +471,7 @@ Підписано Надіслати відгук Деталі + Ви можете мати лише один мобільний гаманець. Оновіть його до апаратного гаманця Tangem або додайте новий апаратний гаманець. Перевірте підключення до інтернету або змініть мережу Умови використання Основна адреса @@ -475,6 +481,15 @@ Надсилання активів в інші мережі призведе до безповоротної втрати. %s мережа Надсилайте кошти, використовуючи лише + Найкращі можливості + Очистити фільтр + Всі мережі + Всі типи + Фільтрувати за + Мої мережі + Мережі + Часто використовані + Немає результатів Привіт, команда підтримки, я зіткнувся з помилкою з кодом: %s Помилка WalletConnect Ви використали картку або кільце від іншого гаманця. Прикладіть картку або кільце, пов\'язану з цим гаманцем. @@ -555,6 +570,10 @@ ID: %s ID транзакції скопійовано Обміняйте будь-який актив у своєму портфелі на цей токен + Більш вища швидкість означає більш швидше підтвердження, але і більш вищу мережеву комісію. %s + Обрати швидкість + Оберіть, який токен буде використовуватися для оплати мережевої комісії. %s + Обрати токен Ринок та Новини В тренді Інформація нижче не є обов\'язковою. Ви можете стерти її, якщо бажаєте. @@ -570,6 +589,8 @@ Звернення в підтримку Tangem Не вдається відправити транзакцію Помилка в описі монети + Недостатньо коштів + Комісія за транзакцію Виникла помилка Виникла помилка. Код: %s. Вимагається memo @@ -619,15 +640,20 @@ Спочатку завершіть резервне копіювання Не завершено Інші методи + Збережіть фразу відновлення в безпечному місці та тримайте її в таємниці, щоб захистити свої кошти, а також налаштуйте код доступу для додаткової безпеки. Збережіть фразу відновлення у безпечному місці і тримайте її у таємниці. Фраза відновлення Щоб захистити свій гаманець за допомогою коду доступу, завершіть процес резервного копіювання. Щоб покращити гаманець до апаратного, спочатку створіть резервну копію. + Оновіть свій мобільний гаманець до апаратного гаманця Tangem для найвищого рівня безпеки. Імпортуйте існуючий гаманець або створіть новий. + Оновлення до холодного гаманця Ваші приватні ключі надійно зашифровані та зберігаються на вашому телефоні Ключі зберігаються у застосунку Створіть або відновіть свій гаманець за допомогою вашої фрази відновлення. Резервна копія Мобільний гаманець + Перемістіть свій мобільний гаманець на Tangem картку або кільце у будь який час. + Оновлення до апаратного гаманця Імпортувати існуючий гаманець Ця фраза відновлення вже була імпортована Мобільний гаманець @@ -821,6 +847,7 @@ Обсяг Потягніть вгору або торкніться панелі пошуку, щоб додати токени безпосередньо з маркету Додати токени + Додати токени Збільшуйте дохід з активів, зберігаючи миттєвий доступ до них. %s Активувати режим дохідності Оновіться до версії %1$s, щоб створити мобільний гаманець @@ -1017,6 +1044,7 @@ Відновлення коду доступу Ідентичні картки Код доступу + Ви можете мати лише один мобільний гаманець. Оновіть його до апаратного гаманця Tangem або додайте новий апаратний гаманець. Усі пропозиції Доступно з %s Tangem забезпечує доступ к покупці через сторонніх провайдерів згідно з їхніми умовами @@ -1131,6 +1159,7 @@ Скинути картку Я розумію, що після виконання цієї дії у мене більше не буде доступу до поточного гаманця Я розумію, що не можу використати цю картку для відновлення свого коду доступу на інших картках поточного гаманця + Я розумію, що повністю втрачу доступ до своєї картки Tangem Pay та всіх коштів на ній без можливості відновлення. Скидання до заводських налаштувань призведе до повного видалення гаманця з обраної картки або кільця. Ви не зможете відновити поточний гаманець або використати цю картку або кільце для відновлення коду доступу. Скидання до заводських налаштувань призведе до повного видалення гаманця з обраної картки або кільця. Ви не зможете відновити поточний гаманець. Усі пристрої Tangem було скинуто. @@ -1279,6 +1308,11 @@ Підготуйтеся до сканування кільця або картки, яку ви хочете налаштувати. Забути гаманець Це призведе до видалення гаманця з застосунку. Сам гаманець можна додати знову. + Простий у використанні + Зберігає ваші криптовалюти в безпеці та офлайн. Тонкий, як кредитна картка, безпечніший за банківське сховище. + Без seed-фрази + Найкращий у своєму класі + Холодний гаманець Tangem Ім\'я Змусьте свій токен працювати Мережева комісія — це невелика оплата, необхідна для обробки та підтвердження вашої транзакції в блокчейні. @@ -1444,6 +1478,7 @@ Просто та зручно — обмін токенів в декілька дотиків Легше, ніж будь-коли Обмін через провайдера + Ваші активи Сума включає: \n• комісію постачальника послуг\n• комісію мережі за відправлення %s з біржі назад на адресу користувача. У суму входить:\n- комісія провайдера\n- мережева комісія за відправку %1$s з біржі назад на адресу користувача. \n\nПроскакування провайдера становить до %2$s Сума включає комісію постачальника послуг. @@ -1458,6 +1493,7 @@ Недостатньо коштів Надати дозвіл Обміняти + Обмін... Ви отримаєте Оберіть токен недоступно @@ -1476,7 +1512,9 @@ Заморозити Вашу картку заморожено. Звернутися до підтримки + Причина: %s Інше + Неможливо використовувати на пристроях з root-правами. Завершено Відхилено В очікуванні @@ -1652,6 +1690,7 @@ Виникла помилка. Код помилки: %s. Спробуйте, будь ласка, знову. Якщо проблема знову виникає — зверніться до нашої служби підтримки. Використовуйте %s або відскануйте картку/кільце, щоб отримати доступ до свого гаманця Не вдалося встановити з\'єднання: Цей dApp використовує Wallet Connect версії 1.0, яка не підтримується. Будь ласка, переконайтеся, що dApp підтримує Wallet Connect версії 2.0 для успішного підключення. + Оновлення до апаратного гаманця Будьте в курсі останніх функцій та новин Миттєві сповіщення про транзакції, обміни та важливі оновлення. Сповіщення про транзакції @@ -1799,7 +1838,18 @@ Зрозуміло! Дуже круто! Оновити + Почати міграцію + Копіювати + Щоб зберегти доступ до своїх коштів, розпочніть міграцію згідно з офіційними рекомендаціями Clore. + Підписання повідомлень не підтримується в цій мережі + Неможливо підписати повідомлення. Будь ласка, спробуйте пізніше. Згідно з офіційною документацією Clore, усі монети, отримані до 21 грудня, будуть мігровані в токен Clore (ERC-20); монети, отримані після цієї дати, — ні. Рішення для переказу перебуває в розробці — стежте за оновленнями. + Повідомлення + Відкрити портал клейму + Щоб зберегти доступ до своїх активів, почніть міграцію у відповідності до офіційної інструкції Clore. + Міграція мережі Clore + Підписати + Підпис Міграція мережі Clore Ви перебуваєте в демонстраційному режимі Демонстраційний режим активовано @@ -1981,6 +2031,7 @@ Швидка доставка Почніть в один клік Просто та надійно + Без seed-фрази Простий у використанні Створіть апаратний гаманець з Tangem. Тонкий, як банківська картка, надійний, як банківське сховище. Створіть або імпортуйте програмний гаманець diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 38fbaf5e44..55ca27cb0a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1436,6 +1436,7 @@ The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. Exchange more tokens at better rates directly in your wallet. New Swap Provider Available! + Looking for something else?/nTry searching or explore another crypto! Feel confident with round-the-clock support to help with any issues Always Here Multiple trusted providers in one place—swap any asset effortlessly in your wallet 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/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/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/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/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index aa6fab81a5..774e48671b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -99,6 +99,7 @@ fun getActiveIconRes(blockchainId: 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 } } @@ -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/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/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/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/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 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/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/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/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/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/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/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..52b9499bb1 --- /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..6dd7dc4c6e --- /dev/null +++ b/data/earn/src/main/java/com/tangem/data/earn/converter/EarnTokenConverter.kt @@ -0,0 +1,34 @@ +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 { + + override fun convert(value: EarnResponse): EarnToken { + return EarnToken( + apy = value.apy, + networkId = value.networkId, + 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/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..6bde739432 --- /dev/null +++ b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt @@ -0,0 +1,12 @@ +package com.tangem.data.earn.datastore + +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 = 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 new file mode 100644 index 0000000000..29f0cbc12e --- /dev/null +++ b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt @@ -0,0 +1,12 @@ +package com.tangem.data.earn.datastore + +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 = 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 new file mode 100644 index 0000000000..19674a4f95 --- /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.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.earn.DefaultEarnErrorResolver +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 +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, + earnNetworksStore: EarnNetworksStore, + earnTopTokensStore: EarnTopTokensStore, + earnErrorResolver: EarnErrorResolver, + excludedBlockchains: ExcludedBlockchains, + ): EarnRepository { + return DefaultEarnRepository( + tangemTechApi = tangemTechApi, + dispatchers = dispatchers, + userWalletsListRepository = userWalletsListRepository, + 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 new file mode 100644 index 0000000000..7a78b887bb --- /dev/null +++ b/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt @@ -0,0 +1,163 @@ +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 +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 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, + 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, + networkName = Blockchain.fromNetworkId(dto.networkId)?.fullName.orEmpty(), + ) + } + } + 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 = earnToken.token.decimalCount ?: 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..85d104f727 --- /dev/null +++ b/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt @@ -0,0 +1,142 @@ +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 +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, + networkName = Blockchain.fromNetworkId(dto.networkId)?.fullName.orEmpty(), + ) + } + } + + 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/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/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/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/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/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt index c9171fd5f3..2d20b15a27 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 @@ -8,8 +8,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.* @@ -20,9 +18,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 { @@ -83,12 +79,12 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( } } - private fun getPossibleWalletsForTangemPay(): List { - val wallets = if (hotWalletFeatureToggles.isHotWalletEnabled) { - userWalletsListRepository.userWallets.value - } else { - userWalletsListManager.userWalletsSync - } ?: return emptyList() + private suspend fun getPossibleWalletsForTangemPay(): List { + if (!checkTangemPayEligibility()) { + return emptyList() + } + + val wallets = userWalletsListRepository.userWallets.value ?: return emptyList() return wallets.filter { wallet -> wallet.isMultiCurrency && !wallet.isLocked && wallet.isCompatible() @@ -122,11 +118,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 ea7ba4b3b1..c9d66fa969 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/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/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/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..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. */ @@ -123,6 +133,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/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..5727b0cd34 --- /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..1988d0e166 --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt @@ -0,0 +1,59 @@ +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 +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) + } + } ?: Either.Right(emptyList()) + }.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..65fb391201 --- /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/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/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..880febc265 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnError.kt @@ -0,0 +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 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/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..523ba3ac45 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt @@ -0,0 +1,35 @@ +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: EarnRewardType, + val type: EarnType, + val tokenId: String, + val tokenSymbol: String, + val tokenName: String, + val tokenAddress: String, + val decimalCount: Int?, +) + +enum class EarnRewardType { + APR, APY +} + +enum class EarnType { + STAKING, YIELD +} \ 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..877be4b05a --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnTokenWithCurrency.kt @@ -0,0 +1,17 @@ +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 networkName: String, + 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/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/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/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 3757a72ea5..06baab2842 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/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 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 eb6257bd8c..df80ac1e89 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,42 +2,22 @@ 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.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map class GetSavedWalletsCountUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, - private val useNewRepository: Boolean, ) { @OptIn(ExperimentalCoroutinesApi::class) operator fun invoke(): Flow> { - if (useNewRepository) { - return flowOf(Unit) - .flatMapLatest { - userWalletsListRepository.load() - userWalletsListRepository.userWallets.map { wallets -> requireNotNull(wallets) } - } - } - - return userWalletsListManager.savedWalletsCount - .filter { count -> - if (count == 0) return@filter true - userWalletsListManager.asLockable() ?: return@filter false - return@filter userWalletsListManager.isLockedSync.not() + return flowOf(Unit) + .flatMapLatest { + userWalletsListRepository.load() + userWalletsListRepository.userWallets.map { wallets -> requireNotNull(wallets) } } - .map { - userWalletsListManager.userWalletsSync - } - .distinctUntilChanged() } } \ 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 75f322f635..6460bfbfd7 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,19 +1,14 @@ 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.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam 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.analytics.Settings -import com.tangem.domain.wallets.legacy.UserWalletsListError -import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletsRepository /** @@ -22,10 +17,8 @@ import com.tangem.domain.wallets.repository.WalletsRepository [REDACTED_AUTHOR] */ class SaveWalletUseCase( - private val userWalletsListManager: UserWalletsListManager, private val userWalletsListRepository: UserWalletsListRepository, private val walletsRepository: WalletsRepository, - private val useNewRepository: Boolean, private val analyticsEventHandler: AnalyticsEventHandler, ) { @@ -34,53 +27,36 @@ class SaveWalletUseCase( canOverride: Boolean = false, analyticsSource: AnalyticsParam.ScreensSources? = null, ): Either { - return if (useNewRepository) { - either { - val newUserWallet = - userWalletsListRepository.userWalletsSync().none { it.walletId == userWallet.walletId } - val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride) - .onRight { trackColdWalletAddedIfNeeded(analyticsSource, it) } - .bind() + return either { + val newUserWallet = + userWalletsListRepository.userWalletsSync().none { it.walletId == userWallet.walletId } + val userWallet = userWalletsListRepository.saveWithoutLock(userWallet, canOverride) + .onRight { trackColdWalletAddedIfNeeded(analyticsSource, it) } + .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/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() } } 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 9596dcee32..6e840ea584 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 @@ -33,7 +33,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 @@ -65,7 +64,6 @@ internal class CreateWalletStartModel @Inject constructor( private val urlOpener: UrlOpener, private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, - private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val appsFlyerStore: AppsFlyerStore, ) : Model() { @@ -101,7 +99,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 f31f23da03..05292329ad 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 @@ -35,7 +34,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 @@ -60,7 +58,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, @@ -69,7 +66,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() { @@ -240,14 +236,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) } } @@ -281,12 +273,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 @@ -294,7 +280,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 4a60a74a45..fbf60da951 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, ) @@ -67,67 +65,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 59be5a0529..1db2f97cc0 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, @@ -84,11 +82,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)) { @@ -116,7 +110,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/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/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/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/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/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/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/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/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/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/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/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/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 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..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 @@ -4,57 +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( @@ -69,7 +50,8 @@ internal fun FeedListHeader( modifier = modifier .drawBehind { drawRect(background) } .padding(horizontal = 16.dp) - .padding(bottom = 8.dp), + .padding(bottom = 8.dp) + .testTag(SEARCH_BAR), ) } @@ -164,6 +146,11 @@ private fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) { trendingArticle = state.trendingArticle, ) + EarnBlock( + onSeeAllClick = state.feedListCallbacks.onOpenEarnPageClick, + earnListUM = state.earnListUM, + ) + MarketPulseBlock( marketChartConfig = state.marketChartConfig, feedListCallbacks = state.feedListCallbacks, @@ -171,400 +158,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.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) }, - ) - } - } - } - } - } -} - -@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/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/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 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( 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( 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 91d539dcc8..767f2d00ec 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,7 +35,6 @@ 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 @@ -43,7 +42,6 @@ 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.feature.referral.domain.ShouldShowMobileWalletPromoUseCase -import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.Debouncer import kotlinx.collections.immutable.toImmutableList @@ -72,8 +70,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, private val shouldShowMobileWalletPromoUseCase: ShouldShowMobileWalletPromoUseCase, @@ -233,7 +229,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, ), @@ -241,14 +237,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/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/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/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/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/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 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/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() { 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/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) { 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/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/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 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), ) 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/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/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index d6ec5bf513..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 @@ -44,7 +44,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 @@ -89,9 +88,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, @@ -149,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() { @@ -206,56 +214,44 @@ internal class WalletModel @Inject constructor( it.copy(showMarketsOnboarding = true) } } - - shouldShowMarketsTooltipUseCase(isShown = true) } } 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(), - referralId = appsFlyerStore.get()?.refcode, - ), - ) + 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(), + referralId = appsFlyerStore.get()?.refcode, + ), + ) + } } } 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 { @@ -301,20 +297,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 58d1a6866a..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 @@ -7,13 +7,13 @@ 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 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 +33,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 @@ -47,7 +46,7 @@ internal interface WalletContentClickIntents { fun onOrganizeTokensClick() - fun onDismissMarketsOnboarding() + fun onDismissMarketsTooltip() fun onTokenItemClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) @@ -90,7 +89,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,51 +98,21 @@ 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, + private val uiMessageSender: UiMessageSender, ) : 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() { router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) } - override fun onDismissMarketsOnboarding() { + override fun onDismissMarketsTooltip() { stateHolder.update { it.copy(showMarketsOnboarding = false) } modelScope.launch { shouldShowMarketsTooltipUseCase(isShown = true) @@ -345,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 55dd5e9a5c..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 @@ -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 @@ -37,22 +37,14 @@ import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.PromotionBannerClicked import com.tangem.domain.tokens.model.details.NavigationAction -import com.tangem.domain.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 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.WalletBottomSheetConfig +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.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 @@ -75,12 +67,6 @@ internal interface WalletWarningsClickIntents { fun onOpenUnlockWalletsBottomSheetClick() - fun onUnlockWalletClick() - - fun onUnlockVisaAccessClick() - - fun onScanToUnlockWalletClick() - fun onLikeAppClick() fun onDislikeAppClick() @@ -93,6 +79,8 @@ internal interface WalletWarningsClickIntents { fun onSupportClick() + fun onBackupErrorClick() + fun onNoteMigrationButtonClick(url: String) fun onSeedPhraseNotificationConfirm() @@ -122,8 +110,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val neverToSuggestRateAppUseCase: NeverToSuggestRateAppUseCase, 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, @@ -137,13 +123,13 @@ 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, private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase, private val getWalletsListForEnablingUseCase: GetWalletsForAutomaticallyPushEnablingUseCase, private val uiMessageSender: UiMessageSender, + private val reviewManager: ReviewManager, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { @@ -205,81 +191,20 @@ 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) - } - } - - override fun onUnlockVisaAccessClick() { - 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()) - .onLeft { error -> - when (error) { - ScanCardToUnlockWalletError.WrongCardIsScanned -> { - walletEventSender.send( - event = WalletEvent.ShowAlert(WalletAlertState.WrongCardIsScanned), + modelScope.launch { + userWalletsListRepository.unlockAllWallets() + .onLeft { + val selectedUserWalletId = stateHolder.getSelectedWalletId() + nonBiometricUnlockWalletUseCase(selectedUserWalletId) + .onLeft { error -> + error.handle( + onAlreadyUnlocked = {}, + onUserCancelled = {}, + analyticsEventHandler = analyticsEventHandler, + isFromUnlockAll = true, + showMessage = uiMessageSender::send, ) } - ScanCardToUnlockWalletError.ManyScanFails -> router.openScanFailedDialog(::openScanCardDialog) - } } } } @@ -287,15 +212,11 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( 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() { @@ -345,7 +266,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( action = PromotionBannerClicked.BannerAction.Closed(), ) }, - ) modelScope.launch(dispatchers.main) { shouldShowPromoWalletUseCase.neverToShow(promoId) @@ -414,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) { @@ -426,21 +355,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), + ) + } + }, ) } @@ -449,17 +373,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) + } + }, ) } @@ -468,21 +387,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/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 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/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 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, ) 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/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/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/WalletAlertState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt deleted file mode 100644 index 3e34289d7f..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletAlertState.kt +++ /dev/null @@ -1,113 +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 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, - ) : 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/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/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/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..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 @@ -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, @@ -422,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, ) } }, @@ -456,7 +446,7 @@ private inline fun BaseScaffoldWithMarkets( LaunchedEffect(state.showMarketsOnboarding, bottomSheetState.targetValue) { if (state.showMarketsOnboarding && bottomSheetState.targetValue == TangemSheetValue.Expanded) { - state.onDismissMarketsOnboarding() + state.onDismissMarketsTooltip() } } } @@ -467,6 +457,7 @@ private fun MarketsTooltip( availableHeight: Dp, bottomSheetState: TangemSheetState, isVisible: Boolean, + onCloseClick: () -> Unit, modifier: Modifier = Modifier, ) { val density = LocalDensity.current @@ -507,7 +498,7 @@ private fun MarketsTooltip( ) + fadeIn(), exit = fadeOut(), ) { - MarketsTooltipContent() + MarketsTooltipContent(onCloseClick = onCloseClick) } } @@ -540,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 { @@ -568,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, ) } } @@ -615,7 +630,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 +648,7 @@ private fun BottomSheetStateEffects( val isKeyboardVisible by rememberIsKeyboardVisible() LaunchedEffect(isKeyboardVisible) { - if (isKeyboardVisible && alertConfig == null && isSearchFieldFocused) { + if (isKeyboardVisible && isSearchFieldFocused) { bottomSheetState.expand() } } 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 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 bc5b6dc548..3aa198a9d7 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 @@ -257,7 +257,7 @@ internal class WelcomeModel @Inject constructor( return canUseBiometryUseCase.strict() && walletsRepository.useBiometricAuthentication() } - suspend fun nonBiometricUnlockWallet(userWalletId: UserWalletId) { + private suspend fun nonBiometricUnlockWallet(userWalletId: UserWalletId) { nonBiometricUnlockWalletUseCase(userWalletId) .onRight { routedOut = true diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 1cc9fdda26..6d72aefb0d 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-1419" +tangemBlockchainSdk = "develop-1425" #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 } 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 diff --git a/tangem-android-tools b/tangem-android-tools index dc9df17919..35755ad66f 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit dc9df17919126bce93482559fe26602f9801eb42 +Subproject commit 35755ad66f5f8fa6bcb1c84b443826cd315495d0