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/KNode.kt b/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt index 7c275607c9..004e367549 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/extensions/KNode.kt @@ -1,5 +1,6 @@ package com.tangem.common.extensions +import androidx.compose.ui.test.hasText import io.github.kakaocup.compose.node.element.KNode fun KNode.clickWithAssertion() { @@ -7,3 +8,13 @@ fun KNode.clickWithAssertion() { performClick() } +fun KNode.assertTextContainsSafe( + text: String, + substring: Boolean = false, + ignoreCase: Boolean = false, +) { + assert( + hasText(text = text, substring = substring, ignoreCase = ignoreCase) + ) +} + diff --git a/app/src/androidTest/kotlin/com/tangem/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..0e2ea89335 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt @@ -1,5 +1,6 @@ package com.tangem.screens +import androidx.compose.ui.semantics.SemanticsProperties import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.SemanticsNodeInteractionsProvider @@ -186,7 +187,8 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) } val totalBalanceText: KNode = child { - hasParent(withTestTag(MainScreenTestTags.WALLET_BALANCE)) + hasAnyAncestor(withTestTag(MainScreenTestTags.WALLET_BALANCE)) + addSemanticsMatcher(SemanticsMatcher.keyIsDefined(SemanticsProperties.Text)) } val notificationYesButton: KNode = child { @@ -235,6 +237,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/SwapChooseTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt new file mode 100644 index 0000000000..e8e256017c --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt @@ -0,0 +1,32 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.R +import com.tangem.core.ui.test.TokenElementsTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasText as withText + +class SwapChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasText(getResourceString(R.string.common_choose_token)) + } + + val myTokensTitle: KNode = child { + hasText(getResourceString(R.string.exchange_tokens_available_tokens_header)) + } + + fun tokenWithTitle(tokenTitle: String): KNode = child { + hasTestTag(TokenElementsTestTags.TOKEN_TITLE) + hasAnyDescendant(withText(tokenTitle)) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onSwapChooseTokenScreen(function: SwapChooseTokenPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/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..15be0dee77 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt @@ -9,10 +9,15 @@ import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onCompose import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.kakao.common.utilities.getResourceString import androidx.compose.ui.test.hasTestTag as withTestTag +import androidx.compose.ui.test.hasText as withText class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { + val container: KNode = child { + hasTestTag(SwapTokenScreenTestTags.CONTAINER) + } + val title: KNode = child { hasTestTag(TopAppBarTestTags.TITLE) hasText(getResourceString(R.string.common_swap)) @@ -76,10 +81,45 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) hasText(getResourceString(R.string.common_swap)) } - fun tokenSymbol(symbol: String): KNode = child { - hasTestTag(SwapTokenScreenTestTags.TOKEN_SYMBOL) + val youSwapBlock: KNode = child { + hasTestTag(SwapTokenScreenTestTags.SWAP_BLOCK_HEADER) + hasAnyDescendant(withText(getResourceString(R.string.swapping_from_title))) + hasAnyDescendant(withTestTag(SwapTokenScreenTestTags.BALANCE)) + useUnmergedTree = true } + val youReceiveBlock: KNode = child { + hasTestTag(SwapTokenScreenTestTags.SWAP_BLOCK_HEADER) + hasAnyDescendant(withText(getResourceString(R.string.swapping_to_title))) + hasAnyDescendant(withTestTag(SwapTokenScreenTestTags.BALANCE)) + useUnmergedTree = true + } + + val receiveFiatAmount: KNode = child { + hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT) + } + + val swapFiatAmount: KNode = child { + hasTestTag(SwapTokenScreenTestTags.SWAP_FIAT_AMOUNT) + } + + val changeTokenIcon: KNode = child { + hasTestTag(SwapTokenScreenTestTags.SELECT_TOKEN_ICON) + } + + fun swapTokenSymbol(symbol: String): KNode = child { + hasAnyAncestor(withTestTag(SwapTokenScreenTestTags.SWAP_CARD)) + hasTestTag(SwapTokenScreenTestTags.TOKEN_SYMBOL) + hasText(symbol) + useUnmergedTree = true + } + + fun receiveTokenSymbol(symbol: String): KNode = child { + hasAnyAncestor(withTestTag(SwapTokenScreenTestTags.RECEIVE_CARD)) + hasTestTag(SwapTokenScreenTestTags.TOKEN_SYMBOL) + hasText(symbol) + useUnmergedTree = true + } } internal fun BaseTestCase.onSwapTokenScreen(function: SwapTokenPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt index a618a5b1dc..3c44cb4154 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt @@ -1,6 +1,7 @@ package com.tangem.tests import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.assertTextContainsSafe import com.tangem.common.extensions.clickWithAssertion import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState @@ -374,7 +375,9 @@ class BuyTokenTest : BaseTestCase() { onSelectProviderBottomSheet { paymentMethodIcon.assertIsDisplayed() } } step("Assert provider name: '$providerNameMercuryo'") { - onSelectProviderBottomSheet { providerName.assertTextContains(providerNameMercuryo) } + onSelectProviderBottomSheet { + providerName.assertTextContainsSafe(text = providerNameMercuryo, substring = true) + } } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt index e807c84361..28fe6ef184 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/MainScreenActionButtonsTest.kt @@ -208,7 +208,7 @@ class MainScreenActionButtonsTest : BaseTestCase() { onSwapTokenScreen { title.assertIsDisplayed() } } step("Assert token symbol: '$tokenSymbol' is displayed") { - onSwapTokenScreen { tokenSymbol(tokenSymbol).assertIsDisplayed() } + onSwapTokenScreen { swapTokenSymbol(tokenSymbol).assertIsDisplayed() } } } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/actionButtons/TokenDetailsScreenActionButtonsTest.kt index d14e14e295..5a158f6e07 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() } } } } @@ -120,7 +119,7 @@ class TokenDetailsScreenActionButtonsTest : BaseTestCase() { onSwapTokenScreen { title.assertIsDisplayed() } } step("Assert token symbol: '$tokenSymbol' is displayed") { - onSwapTokenScreen { tokenSymbol(tokenSymbol).assertIsDisplayed() } + onSwapTokenScreen { swapTokenSymbol(tokenSymbol).assertIsDisplayed() } } } } @@ -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/TotalBalanceLongTapTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceLongTapTest.kt index 7eea7bf682..bae6abd799 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceLongTapTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceLongTapTest.kt @@ -31,8 +31,8 @@ class TotalBalanceLongTapTest : BaseTestCase() { } } } - step("Assert 'Rename' button is not displayed") { - onMainScreen { totalBalanceMenuRenameWallet.assertIsNotDisplayed() } + step("Assert 'Rename' button is displayed") { + onMainScreen { totalBalanceMenuRenameWallet.assertIsDisplayed() } } step("Assert 'Delete' button is not displayed") { onMainScreen { totalBalanceMenuDeleteWallet.assertIsNotDisplayed() } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceUpdateTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/balance/TotalBalanceUpdateTest.kt index eca72183ce..2003821062 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,17 @@ 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() } } 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..21e65feb9b --- /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 not 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 not 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("5470") + @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 not 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 50% 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..2048e0fa7e 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 @@ -10,7 +10,9 @@ import com.tangem.common.extensions.* import com.tangem.common.utils.resetWireMockScenarios import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.scenarios.SwapEntryPoint import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.openSwapScreen import com.tangem.scenarios.synchronizeAddresses import com.tangem.screens.* import dagger.hilt.android.testing.HiltAndroidTest @@ -19,7 +21,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) @@ -28,7 +30,7 @@ class SwapTokenTest : BaseTestCase() { @DisplayName("Swap: network fee") @Test fun networkFeeTest() { - val inputAmount = "100" + val inputAmount = "400" val tokenTitle = "Polygon" setupHooks().run { @@ -43,7 +45,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") { @@ -97,22 +99,31 @@ class SwapTokenTest : BaseTestCase() { } } step("Assert receive amount is not equal to '0'") { - onSwapTokenScreen { receiveAmount.assert(!hasText("0")) } + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + waitForIdle() + receiveAmount.assert(!hasText("0")) + } + } } } } + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) @AllureId("3549") @DisplayName("Swap: network error test") @Test fun networkErrorSwapTest() { + val tokenTitle = "Polygon" + setupHooks( additionalAfterSection = { enableWiFi() enableMobileData() } ).run { - val tokenTitle = "Polygon" step("Open 'Main Screen'") { openMainScreen() @@ -123,7 +134,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") { @@ -140,7 +151,12 @@ class SwapTokenTest : BaseTestCase() { onSwapTokenScreen { title.assertIsDisplayed() } } step("Assert error notification title is displayed") { - onSwapTokenScreen { errorNotificationTitle.assertIsDisplayed() } + onSwapTokenScreen { + waitForIdle() + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + errorNotificationTitle.assertIsDisplayed() + } + } } step("Assert error notification text is displayed") { onSwapTokenScreen { errorNotificationText.assertIsDisplayed() } @@ -158,7 +174,8 @@ class SwapTokenTest : BaseTestCase() { @DisplayName("Swap: change network fee") @Test fun changeNetworkFeeTest() { - val inputAmount = "100" + val inputAmount = "400" + setupHooks().run { val tokenTitle = "Polygon" @@ -171,7 +188,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") { @@ -207,10 +224,20 @@ class SwapTokenTest : BaseTestCase() { step("Assert input amount = '$inputAmount'") { onSwapTokenScreen { textInput.assertTextEquals(inputAmount) } } + step("Assert 'Network fee' block is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + networkFeeBlock.assertIsDisplayed() + } + } + } + step("Assert 'Swap' button is enabled") { + onSwapTokenScreen { swapButton.assertIsEnabled() } + } step("Click on 'Network fee' block") { onSwapTokenScreen { flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { - selectFeeIcon.clickWithAssertion() + selectFeeIcon.performClick() } } } @@ -218,7 +245,6 @@ class SwapTokenTest : BaseTestCase() { onSwapSelectNetworkFeeBottomSheet { title.assertIsDisplayed() } } step("Assert 'Market' item is displayed") { - printSemanticTree(rootIndex = 1, useUnmergedTree = true) onSwapSelectNetworkFeeBottomSheet { marketSelectorItem.assertIsDisplayed() } } step("Assert 'Fast' item is displayed") { @@ -239,4 +265,183 @@ 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 { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() } + } + step("Assert token symbol: '$receiveTokenSymbol' is displayed") { + onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() } + } + } + } + + @AllureId("575") + @DisplayName("Swap: check UI") + @Test + fun checkSwapUiTest() { + val swapTokenSymbol = "POL" + val receiveTokenSymbol = "ETH" + val newReceiveToken = "POL (ex-MATIC)" + val tokenTitle = "Polygon" + val inputAmount = "1" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'Close' button is displayed") { + onSwapTokenScreen { closeButton.assertIsDisplayed() } + } + step("Assert swap token symbol: '$swapTokenSymbol' is displayed") { + onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() } + } + step("Assert receive token symbol: '$receiveTokenSymbol' is displayed") { + onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() } + } + step("Click on 'Select token' icon") { + onSwapTokenScreen { changeTokenIcon.performClick() } + } + step("Select new receive token: $newReceiveToken") { + onSwapChooseTokenScreen { tokenWithTitle(newReceiveToken).performClick() } + } + step("Assert new receive token symbol: '$swapTokenSymbol' is displayed") { + onSwapTokenScreen { receiveTokenSymbol(swapTokenSymbol).assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert input amount = '$inputAmount'") { + onSwapTokenScreen { textInput.assertTextEquals(inputAmount) } + } + step("Press 'Delete' button on keyboard") { + device.uiDevice.pressDelete() + waitForIdle() + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Assert 'You receive' block is displayed") { + onSwapTokenScreen { youReceiveBlock } + } + step("Assert send token fiat amount is displayed") { + onSwapTokenScreen { swapFiatAmount.assertIsDisplayed() } + } + step("Assert receive token fiat amount is displayed") { + onSwapTokenScreen { receiveFiatAmount.assertIsDisplayed() } + } + step("Assert 'Swap tokens on screen' button is displayed") { + onSwapTokenScreen { + flakySafely(WAIT_UNTIL_TIMEOUT) { + swapTokensOnscreenButton.assertIsDisplayed() + } + } + } + step("Assert 'Swap' button is disabled") { + onSwapTokenScreen { swapButton.assertIsNotEnabled() } + } + } + } + + @AllureId("5162") + @DisplayName("Swap: check swap tokens switch") + @Test + fun checkSwapTokensSwitchTest() { + val swapTokenSymbol = "POL" + val receiveTokenSymbol = "ETH" + val tokenTitle = "Polygon" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert swap token symbol: '$swapTokenSymbol' is displayed") { + onSwapTokenScreen { swapTokenSymbol(swapTokenSymbol).assertIsDisplayed() } + } + step("Assert receive token symbol: '$receiveTokenSymbol' is displayed") { + onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() } + } + step("Click on 'Swap tokens on screen' button") { + onSwapTokenScreen { swapTokensOnscreenButton.performClick() } + waitForIdle() + } + step("Assert new swap token symbol: '$receiveTokenSymbol' is displayed") { + onSwapTokenScreen { swapTokenSymbol(receiveTokenSymbol).assertIsDisplayed() } + } + step("Assert new receive token symbol: '$swapTokenSymbol' is displayed") { + onSwapTokenScreen { receiveTokenSymbol(swapTokenSymbol).assertIsDisplayed() } + } + } + } } \ 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/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 97a76b6ac8..2ef3c24014 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -313,6 +313,16 @@ + + + + + + + + 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/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index 4dc9cefd1a..0d9a4eba92 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -1,7 +1,12 @@ package com.tangem.tap.data import android.content.Context +import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import com.tangem.data.pay.entity.WithdrawStoreData +import com.tangem.data.pay.util.WithdrawStateConverter +import com.tangem.data.pay.util.WithdrawStoreDataConverter import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -11,10 +16,12 @@ import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.TangemPayWithdrawState import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.withContext import java.util.UUID import javax.inject.Inject @@ -41,6 +48,18 @@ internal class DefaultTangemPayStorage @Inject constructor( } private val tokensAdapter by lazy { moshi.adapter(TangemPayAuthTokens::class.java) } + private val withdrawStoreDataConverter by lazy { WithdrawStoreDataConverter() } + private val withdrawStateConverter by lazy { WithdrawStateConverter() } + + private val listType by lazy { + Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java) + } + private val mapType by lazy { + Types.newParameterizedType(Map::class.java, String::class.java, listType) + } + private val adapter: JsonAdapter>> by lazy { + appPreferencesStore.moshi.adapter(mapType) + } override suspend fun storeCustomerWalletAddress(userWalletId: UserWalletId, customerWalletAddress: String) { withContext(dispatcherProvider.io) { @@ -131,33 +150,76 @@ internal class DefaultTangemPayStorage @Inject constructor( return appPreferencesStore.getSyncOrNull(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId)) } - override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String) { + override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) { appPreferencesStore.editData { mutablePreferences -> - val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY) + val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) .plus(createWithdrawOrderIdKey(userWalletId) to orderId) mutablePreferences.setObjectMap( - key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, + key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, value = orders, ) } } - override suspend fun getWithdrawOrderId(userWalletId: UserWalletId): String? { - val orders = appPreferencesStore.getObjectMapSync(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY) + override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) { + appPreferencesStore.editData { prefs -> + val walletKey = createWithdrawOrderIdKey(userWalletId) + val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] + ?.let(adapter::fromJson) + .orEmpty() + val newItem = withdrawStoreDataConverter.convert(data) + val currentList = currentMap[walletKey].orEmpty() + val updatedList = buildList(currentList.size + 1) { + for (item in currentList) { if (item.orderId != newItem.orderId) add(item) } + add(newItem) + } + prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = + adapter.toJson(currentMap + (walletKey to updatedList)) + } + } + + override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? { + val orders = appPreferencesStore.getObjectMapSync(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) return orders[createWithdrawOrderIdKey(userWalletId)] } - override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) { + override suspend fun getWithdrawOrders(userWalletId: UserWalletId): List { + val map = appPreferencesStore.data.firstOrNull() + ?.get(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)?.let(adapter::fromJson).orEmpty() + return map[createWithdrawOrderIdKey(userWalletId)].orEmpty().map(withdrawStateConverter::convert) + } + + override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) { appPreferencesStore.editData { mutablePreferences -> - val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY) + val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) .minus(createWithdrawOrderIdKey(userWalletId)) mutablePreferences.setObjectMap( - key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, + key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, value = orders, ) } } + override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) { + appPreferencesStore.editData { prefs -> + val walletKey = createWithdrawOrderIdKey(userWalletId) + val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]?.let(adapter::fromJson) + .orEmpty() + val currentList = currentMap[walletKey].orEmpty() + val updatedList = buildList(currentList.size) { + for (item in currentList) { + if (item.orderId != orderId) add(item) + } + } + val updatedMap = if (updatedList.isEmpty()) { + currentMap - walletKey + } else { + currentMap + (walletKey to updatedList) + } + prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = adapter.toJson(updatedMap) + } + } + override suspend fun storeHideOnboardingBanner(userWalletId: UserWalletId, hide: Boolean) { withContext(dispatcherProvider.io) { appPreferencesStore.store(PreferencesKeys.getTangemPayHideOnboardingKey(userWalletId), hide) 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/data/UserWalletsStoreRepositoryProxy.kt b/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt deleted file mode 100644 index f167597c1a..0000000000 --- a/app/src/main/java/com/tangem/tap/data/UserWalletsStoreRepositoryProxy.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.tangem.tap.data - -import com.tangem.common.CompletionResult -import com.tangem.common.catching -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow - -class UserWalletsStoreRepositoryProxy( - private val userWalletsListRepository: UserWalletsListRepository, -) : UserWalletsStore { - - override val selectedUserWalletOrNull: UserWallet? - get() = userWalletsListRepository.selectedUserWallet.value - - override val userWallets: Flow> - get() = flow { - userWalletsListRepository.load() - userWalletsListRepository.userWallets.collect { - emit(requireNotNull(it)) - } - } - - override val userWalletsSync: List - get() = userWalletsListRepository.userWallets.value.orEmpty() - - override fun getSyncOrNull(key: UserWalletId): UserWallet? { - return userWalletsListRepository.userWallets.value?.find { it.walletId == key } - } - - override fun getSyncStrict(key: UserWalletId): UserWallet { - return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" } - } - - override suspend fun update( - userWalletId: UserWalletId, - update: suspend (UserWallet) -> UserWallet, - ): CompletionResult { - return catching { - val userWallet = userWalletsListRepository.userWallets.value?.find { it.walletId == userWalletId } - requireNotNull(userWallet) { "Unable to find user wallet with provided ID: $userWalletId" } - val updatedUserWallet = update(userWallet) - userWalletsListRepository.saveWithoutLock( - userWallet = updatedUserWallet, - canOverride = true, - ) - updatedUserWallet - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt b/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt deleted file mode 100644 index f08b09fb11..0000000000 --- a/app/src/main/java/com/tangem/tap/di/data/UserWalletsStoreModule.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.tap.di.data - -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.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 -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object UserWalletsStoreModule { - - @Provides - @Singleton - fun provideUserWalletsStore( - userWalletsListManager: UserWalletsListManager, - userWalletsListRepository: UserWalletsListRepository, - hotWalletFeatureToggles: HotWalletFeatureToggles, - ): UserWalletsStore { - return if (hotWalletFeatureToggles.isHotWalletEnabled) { - UserWalletsStoreRepositoryProxy(userWalletsListRepository) - } else { - RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager) - } - } -} \ 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..3ae0f6d5c6 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt @@ -0,0 +1,58 @@ +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) + } + + @Provides + fun provideGetEarnFilterUseCase(repository: EarnRepository): GetEarnFilterUseCase { + return GetEarnFilterUseCase(repository) + } + + @Provides + fun provideSetEarnFilterUseCase(repository: EarnRepository): SetEarnFilterUseCase { + return SetEarnFilterUseCase(repository) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt index 8c8404b2d3..f3ee5bef10 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt @@ -1,9 +1,13 @@ package com.tangem.tap.di.domain +import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase +import com.tangem.domain.hotwallet.CloseHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase +import com.tangem.domain.hotwallet.GetUpgradeBannerClosureTimestampUseCase import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.hotwallet.IsAccessCodeSimpleUseCase import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase +import com.tangem.domain.hotwallet.ShouldShowUpgradeHotWalletBannerUseCase import com.tangem.domain.hotwallet.repository.HotWalletRepository import dagger.Module import dagger.Provides @@ -40,4 +44,36 @@ internal object HotWalletDomainModule { ): IsHotWalletCreationSupported { return IsHotWalletCreationSupported(hotWalletRepository) } + + @Provides + @Singleton + fun provideCheckHotWalletUpgradeBannerUseCase( + hotWalletRepository: HotWalletRepository, + ): CheckHotWalletUpgradeBannerUseCase { + return CheckHotWalletUpgradeBannerUseCase(hotWalletRepository) + } + + @Provides + @Singleton + fun provideCloseHotWalletUpgradeBannerUseCase( + hotWalletRepository: HotWalletRepository, + ): CloseHotWalletUpgradeBannerUseCase { + return CloseHotWalletUpgradeBannerUseCase(hotWalletRepository) + } + + @Provides + @Singleton + fun provideShouldShowUpgradeHotWalletBannerUseCase( + hotWalletRepository: HotWalletRepository, + ): ShouldShowUpgradeHotWalletBannerUseCase { + return ShouldShowUpgradeHotWalletBannerUseCase(hotWalletRepository) + } + + @Provides + @Singleton + fun provideGetUpgradeBannerClosureTimestampUseCase( + hotWalletRepository: HotWalletRepository, + ): GetUpgradeBannerClosureTimestampUseCase { + return GetUpgradeBannerClosureTimestampUseCase(hotWalletRepository) + } } \ 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/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 55dab8c3b9..52ecf3a396 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -5,6 +5,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier +import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.domain.tokens.GetMultiCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase @@ -56,6 +57,7 @@ internal object TransactionDomainModule { singleNetworkStatusFetcher: SingleNetworkStatusFetcher, tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory, dispatchers: CoroutineDispatcherProvider, + pushNotificationsRepository: PushNotificationsRepository, ): SendTransactionUseCase { return SendTransactionUseCase( demoConfig = DemoConfig, @@ -65,6 +67,7 @@ internal object TransactionDomainModule { singleNetworkStatusFetcher = singleNetworkStatusFetcher, parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.io), getHotWalletSigner = tangemHotWalletSignerFactory::create, + pushNotificationsRepository = pushNotificationsRepository, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index d84fe82788..5e55ebdd54 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -1,9 +1,9 @@ package com.tangem.tap.di.domain import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase @@ -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.WalletsPromoRepository import com.tangem.domain.wallets.repository.WalletsRepository @@ -23,7 +22,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 @@ -39,105 +37,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, ) } @@ -145,15 +99,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 @@ -170,12 +118,6 @@ internal object WalletsDomainModule { return GetExploreUrlUseCase(walletsManagersFacade = walletsManagersFacade) } - @Provides - @Singleton - fun providesUnlockWalletsUseCase(userWalletsListManager: UserWalletsListManager): UnlockWalletsUseCase { - return UnlockWalletsUseCase(userWalletsListManager = userWalletsListManager) - } - @Provides @Singleton fun providesUnlockWalletUseCase( @@ -203,31 +145,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 @@ -244,44 +174,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 @@ -353,15 +253,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 @@ -378,11 +272,11 @@ internal object WalletsDomainModule { @Singleton fun providesSetNotificationsEnabledUseCase( walletsRepository: WalletsRepository, - currenciesRepository: CurrenciesRepository, + accountsCRUDRepository: AccountsCRUDRepository, ): SetNotificationsEnabledUseCase { return SetNotificationsEnabledUseCase( walletsRepository = walletsRepository, - currenciesRepository = currenciesRepository, + accountsCRUDRepository = accountsCRUDRepository, ) } @@ -485,15 +379,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, ) } @@ -511,4 +401,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/scanCard/DefaultScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt index 216af3ca6d..f04b03cef6 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/DefaultScanCardProcessor.kt @@ -20,6 +20,7 @@ internal class DefaultScanCardProcessor( cardId: String?, allowsRequestAccessCodeFromRepository: Boolean, analyticsSource: AnalyticsParam.ScreensSources, + shouldCheckIsAlreadyActivated: Boolean, ): CompletionResult { return if (isNewCardScanningEnabled) { UseCaseScanProcessor.scan(cardId, allowsRequestAccessCodeFromRepository) @@ -28,6 +29,7 @@ internal class DefaultScanCardProcessor( analyticsSource = analyticsSource, cardId = cardId, allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, + shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated, ) } } @@ -35,6 +37,7 @@ internal class DefaultScanCardProcessor( @Suppress("LongParameterList") override suspend fun scan( analyticsSource: AnalyticsParam.ScreensSources, + shouldCheckIsAlreadyActivated: Boolean, cardId: String?, onProgressStateChange: suspend (showProgress: Boolean) -> Unit, onWalletNotCreated: suspend () -> Unit, @@ -56,6 +59,7 @@ internal class DefaultScanCardProcessor( } else { legacyScanProcessor.scan( analyticsSource = analyticsSource, + shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated, cardId = cardId, onProgressStateChange = onProgressStateChange, onWalletNotCreated = onWalletNotCreated, diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index 95d2b8fe04..fd2e386a76 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -19,15 +19,15 @@ import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList import com.tangem.core.ui.message.dialog.Dialogs -import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.card.common.util.twinsIsTwinned +import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.sdk.extensions.localizedDescriptionRes import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor -import com.tangem.tap.common.extensions.* -import com.tangem.tap.common.redux.AppDialog +import com.tangem.tap.common.extensions.dispatchNavigationAction +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.onboarding.OnboardingHelper @@ -55,10 +55,12 @@ internal class LegacyScanProcessor @Inject constructor( cardId: String? = null, allowsRequestAccessCodeFromRepository: Boolean = false, analyticsSource: AnalyticsParam.ScreensSources, + shouldCheckIsAlreadyActivated: Boolean, ): CompletionResult { return tangemSdkManager.scanProduct( cardId = cardId, allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, + shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated, ) .doOnFailure { error -> onScanFailure(analyticsSource = analyticsSource, error = error, onFailure = {}, onCancel = {}) @@ -68,6 +70,7 @@ internal class LegacyScanProcessor @Inject constructor( @Suppress("LongParameterList") suspend fun scan( analyticsSource: AnalyticsParam.ScreensSources, + shouldCheckIsAlreadyActivated: Boolean, cardId: String?, onProgressStateChange: suspend (showProgress: Boolean) -> Unit, onWalletNotCreated: suspend () -> Unit, @@ -80,7 +83,10 @@ internal class LegacyScanProcessor @Inject constructor( tangemSdkManager.changeDisplayedCardIdNumbersCount(null) - val result = tangemSdkManager.scanProduct(cardId) + val result = tangemSdkManager.scanProduct( + cardId = cardId, + shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated, + ) val analyticsEvent = Basic.CardWasScanned(analyticsSource) store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result, analyticsSource)) @@ -113,7 +119,6 @@ internal class LegacyScanProcessor @Inject constructor( onProgressStateChange = onProgressStateChange, onSuccess = onSuccess, onWalletNotCreated = onWalletNotCreated, - onCancel = onCancel, ) }, ) @@ -198,90 +203,38 @@ internal class LegacyScanProcessor @Inject constructor( scanResponse: ScanResponse, crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit, crossinline onWalletNotCreated: suspend () -> Unit, - crossinline onCancel: suspend () -> Unit, crossinline onSuccess: suspend (ScanResponse) -> Unit, ) { - checkCardWasUsedInApp( - scanResponse = scanResponse, - onCancel = { - mainScope.launch { - onProgressStateChange.invoke(false) - onCancel() - } - }, - ) { - if (OnboardingHelper.isOnboardingCase(scanResponse)) { - trackingContextProxy.addContext(scanResponse) + if (OnboardingHelper.isOnboardingCase(scanResponse)) { + trackingContextProxy.addContext(scanResponse) + onWalletNotCreated() + navigateTo( + AppRoute.Onboarding( + scanResponse = scanResponse, + mode = AppRoute.Onboarding.Mode.Onboarding, + ), + ) { onProgressStateChange(it) } + } else { + trackingContextProxy.setContext(scanResponse) + + val wasTwinsOnboardingShown = + store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync() + + if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) { onWalletNotCreated() navigateTo( AppRoute.Onboarding( scanResponse = scanResponse, - mode = AppRoute.Onboarding.Mode.Onboarding, + mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, ), ) { onProgressStateChange(it) } } else { - trackingContextProxy.setContext(scanResponse) - - val wasTwinsOnboardingShown = - store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync() - - if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) { - onWalletNotCreated() - navigateTo( - AppRoute.Onboarding( - scanResponse = scanResponse, - mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, - ), - ) { onProgressStateChange(it) } - } else { - delay(DELAY_SDK_DIALOG_CLOSE) - onSuccess(scanResponse) - } + delay(DELAY_SDK_DIALOG_CLOSE) + onSuccess(scanResponse) } } } - /** - * Checks if card has password and never login at this app - * Show alert in this case - */ - private suspend fun checkCardWasUsedInApp( - scanResponse: ScanResponse, - onCancel: () -> Unit, - onSuccess: suspend () -> Unit, - ) { - val userWalletId = runCatching { UserWalletIdBuilder.card(scanResponse.card).build() }.getOrNull() - if (userWalletId == null) { - onSuccess() - return - } - - val userTokensResponseStore = store.inject(DaggerGraphState::userTokensResponseStore) - val tokens = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - - if (scanResponse.card.isAccessCodeSet && tokens == null) { - store.dispatchDialogShow( - AppDialog.WalletAlreadyWasUsedDialog( - onOk = { mainScope.launch { onSuccess() } }, - onSupportClick = { - val cardInfo = - store.inject(DaggerGraphState::getWalletMetaInfoUseCase).invoke(scanResponse).getOrNull() - ?: error("CardInfo must be not null") - - scope.launch { - store.inject(DaggerGraphState::sendFeedbackEmailUseCase) - .invoke(type = FeedbackEmailType.PreActivatedWallet(cardInfo)) - } - onCancel() - }, - onCancel = { onCancel() }, - ), - ) - } else { - onSuccess() - } - } - private suspend inline fun navigateTo(route: AppRoute, onProgressStateChange: (showProgress: Boolean) -> Unit) { delay(DELAY_SDK_DIALOG_CLOSE) store.dispatchNavigationAction { push(route) } diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt index 1a4b50cc5a..ecd2cffeb0 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/repository/DefaultScanCardRepository.kt @@ -13,11 +13,16 @@ internal class DefaultScanCardRepository( private val exceptionConverter = ScanCardExceptionConverter() - override suspend fun scanCard(cardId: String?, allowRequestAccessCodeFromStorage: Boolean): ScanResponse { + override suspend fun scanCard( + cardId: String?, + allowRequestAccessCodeFromStorage: Boolean, + shouldCheckIsAlreadyActivated: Boolean, + ): ScanResponse { return when ( val result = tangemSdkManager.scanProduct( cardId = cardId, allowsRequestAccessCodeFromRepository = allowRequestAccessCodeFromStorage, + shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated, ) ) { is CompletionResult.Success -> result.data diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 8639981110..aea6c8a351 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -126,7 +126,13 @@ internal class DefaultTangemSdkManager( } if (awaitInitialization) { - awaitAuthenticationManagerInitialization().needEnrollBiometrics + val manager = awaitAuthenticationManagerInitialization() + + if (manager.isInitialized) { + manager.needEnrollBiometrics + } else { + false + } } else { throw e } @@ -145,7 +151,11 @@ internal class DefaultTangemSdkManager( if (awaitInitialization) { val manager = awaitAuthenticationManagerInitialization() - manager.canAuthenticate || manager.needEnrollBiometrics + if (manager.isInitialized) { + manager.canAuthenticate || manager.needEnrollBiometrics + } else { + false + } } else { throw e } @@ -156,6 +166,7 @@ internal class DefaultTangemSdkManager( cardId: String?, messageRes: Int?, allowsRequestAccessCodeFromRepository: Boolean, + shouldCheckIsAlreadyActivated: Boolean, ): CompletionResult { val message = Message(resources.getStringSafe(messageRes ?: R.string.initial_message_scan_header)) return coroutineScope { @@ -166,6 +177,7 @@ internal class DefaultTangemSdkManager( allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, visaCardScanHandler = visaCardScanHandler, visaCoroutineScope = this, + shouldCheckIsAlreadyActivated = shouldCheckIsAlreadyActivated, onboardingV2FeatureToggles = onboardingV2FeatureToggles, ), cardId = cardId, diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index 00e4ea971e..c70dc421f4 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -59,6 +59,7 @@ class MockTangemSdkManager( cardId: String?, messageRes: Int?, allowsRequestAccessCodeFromRepository: Boolean, + shouldCheckIsAlreadyActivated: Boolean, ): CompletionResult { return MockProvider.getScanResponse() } diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt index bb0fafa039..f1a1683517 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt @@ -27,6 +27,7 @@ internal class ResetBackupCardTask( PreflightReadTask( readMode = PreflightReadMode.FullCardRead, filter = UserWalletIdPreflightReadFilter(expectedUserWalletId = userWalletId), + secureStorage = session.environment.secureStorage, ).run(session) { result -> when (result) { is CompletionResult.Success -> resetCard(session, callback) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 93a1c1f2be..4c334fd027 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -29,6 +29,7 @@ import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles +import com.tangem.operations.PreflightReadMode import com.tangem.operations.ScanTask import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.backup.StartPrimaryCardLinkingTask @@ -52,6 +53,7 @@ internal class ScanProductTask( private val visaCardScanHandler: VisaCardScanHandler?, private val visaCoroutineScope: CoroutineScope?, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?, + private val shouldCheckIsAlreadyActivated: Boolean, override val allowsRequestAccessCodeFromRepository: Boolean = false, ) : CardSessionRunnable { @@ -109,6 +111,14 @@ internal class ScanProductTask( } } + override fun preflightReadMode(): PreflightReadMode { + return if (shouldCheckIsAlreadyActivated) { + PreflightReadMode.FullCardReadWithAccessCodeCheck + } else { + return super.preflightReadMode() + } + } + private fun getErrorIfExcludedCard(cardDto: CardDTO, card: Card): TangemError? { if (cardDto.isExcluded) return TapSdkError.CardForDifferentApp() if (cardDto.isNotSupportedInThatRelease) return TapSdkError.CardNotSupportedByRelease() diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt index 2b20696ee9..0f7ba29f04 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt @@ -20,7 +20,10 @@ class FinalizeTwinTask( WriteProtectedIssuerDataTask(twinPublicKey, issuerKeys).run(session) { result -> when (result) { is CompletionResult.Success -> - PreflightReadTask(PreflightReadMode.FullCardRead).run(session) { readResult -> + PreflightReadTask( + readMode = PreflightReadMode.FullCardRead, + secureStorage = session.environment.secureStorage, + ).run(session) { readResult -> when (readResult) { is CompletionResult.Success -> ScanProductTask( @@ -28,6 +31,7 @@ class FinalizeTwinTask( derivationsFinder = null, visaCardScanHandler = null, visaCoroutineScope = null, + shouldCheckIsAlreadyActivated = false, onboardingV2FeatureToggles = null, ).run(session, callback) is CompletionResult.Failure -> diff --git a/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..c75cbbbed9 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -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,18 @@ 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.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext @Suppress("LongParameterList", "LargeClass") internal class DefaultUserWalletsListRepository( @@ -64,6 +64,7 @@ internal class DefaultUserWalletsListRepository( override val userWallets = MutableStateFlow?>(null) override val selectedUserWallet = MutableStateFlow(null) + private val mutex = Mutex() override suspend fun load() { @@ -295,8 +296,9 @@ internal class DefaultUserWalletsListRepository( } val scanResponse = unlockMethod.scanResponse ?: run { - val res = tangemSdkManagerProvider().scanProduct() - when (res) { + when (val res = tangemSdkManagerProvider().scanProduct( + shouldCheckIsAlreadyActivated = false, + )) { is CompletionResult.Failure -> raise(UnlockWalletError.UserCancelled) is CompletionResult.Success -> res.data } @@ -395,6 +397,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 +475,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/UserWalletsKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt deleted file mode 100644 index 0c196308db..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/UserWalletsKeysRepository.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.tap.domain.userWalletList.repository - -import com.tangem.common.CompletionResult -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.tap.domain.userWalletList.model.UserWalletEncryptionKey - -internal interface UserWalletsKeysRepository { - /** - * Obtaining the encryption keys of all user wallets from the biometric vault. Biometric authentication required - * If that operation runs more than biometric cipher key expiration time then the user will not receive all - * encryption keys - * @return [CompletionResult] of operation with stored [UserWalletEncryptionKey] list - * */ - suspend fun getAll(): CompletionResult> - - /** - * Save the encryption key for user wallet. Biometric authentication not required - * @param encryptionKey [UserWalletEncryptionKey] to save - * @return [CompletionResult] of operation - * */ - suspend fun save(encryptionKey: UserWalletEncryptionKey): CompletionResult - - /** - * Delete encryption keys for user wallets. Biometric authentication not required - * @param userWalletsIds List of [UserWalletId] whose encryption keys will be deleted - * */ - suspend fun delete(userWalletsIds: List) - - /** - * Clear all encryption keys for user wallets. Biometric authentication not required - * */ - suspend fun clear() - - /** - * Determine if the user has saved user wallets - * @return [Boolean] true if user has saved wallets - * */ - fun hasSavedEncryptionKeys(): Boolean -} \ No newline at end of file diff --git a/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/cardsettings/model/CardSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt index 797ef6c056..1d9a6b2f6e 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt @@ -107,6 +107,7 @@ internal class CardSettingsModel @Inject constructor( private fun scanCard() = modelScope.launch { scanCardProcessor.scan( analyticsSource = com.tangem.core.analytics.models.AnalyticsParam.ScreensSources.Settings, + shouldCheckIsAlreadyActivated = false, allowsRequestAccessCodeFromRepository = true, ) .doOnSuccess { scanResponse -> diff --git a/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/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index bd69d9835d..285f2c46e9 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -86,7 +86,7 @@ internal class DefaultRampManager( cryptoCurrency: CryptoCurrency, ): ScenarioUnavailabilityReason { val availabilityState = runSuspendCatching { - getExchangeableState(userWalletId, cryptoCurrency) + getExchangeableState() }.getOrNull() ?: ExpressAvailabilityState.Error return availabilityState.toReason(cryptoCurrency.name) } @@ -142,21 +142,9 @@ internal class DefaultRampManager( } } - private suspend fun getExchangeableState( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency, - ): ExpressAvailabilityState { - val asset = expressServiceFetcher.getInitializationStatus(userWalletId).firstOrNull() - ?: return ExpressAvailabilityState.Loading - return when (asset) { - is Lce.Error -> ExpressAvailabilityState.Error - is Lce.Loading -> ExpressAvailabilityState.Loading - is Lce.Content -> { - val foundAsset = asset.getOrNull()?.find { cryptoCurrency.findAssetPredicate(assetId = it.id) } - foundAsset?.isExchangeAvailable?.toSwapAvailabilityState() - ?: ExpressAvailabilityState.AssetNotFound - } - } + private fun getExchangeableState(): ExpressAvailabilityState { + // In task [REDACTED_TASK_KEY], removed all checks to make all tokens available + return ExpressAvailabilityState.Available } private suspend fun getOnrampAvailableState( @@ -190,14 +178,6 @@ internal class DefaultRampManager( } } - private fun Boolean.toSwapAvailabilityState(): ExpressAvailabilityState { - return if (this) { - ExpressAvailabilityState.Available - } else { - ExpressAvailabilityState.NotExchangeable - } - } - private fun Boolean.toOnrampAvailabilityState(): ExpressAvailabilityState { return if (this) { ExpressAvailabilityState.Available diff --git a/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/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt new file mode 100644 index 0000000000..64554a30e0 --- /dev/null +++ b/common/src/main/kotlin/com/tangem/common/TangemSiteShareUrlBuilder.kt @@ -0,0 +1,31 @@ +package com.tangem.common + +import com.tangem.utils.SupportedLanguages.CHINESE +import com.tangem.utils.SupportedLanguages.ENGLISH +import com.tangem.utils.SupportedLanguages.FRANCH +import com.tangem.utils.SupportedLanguages.GERMAN +import com.tangem.utils.SupportedLanguages.JAPANESE +import java.util.Locale + +object TangemSiteShareUrlBuilder { + + private const val BASE_URL = "https://tangem.com" + private const val CRYPTOCURRENCIES_PATH = "cryptocurrencies" + + @Deprecated("Should use CHINESE from SupportedLanguages, but the site expects zh-Hans in the URL path") + private const val CHINESE_SITE_LOCALE = "zh-Hans" + + @Deprecated("Should rely on SupportedLanguages instead of maintaining a separate list") + private val siteLocales = mapOf( + ENGLISH to ENGLISH, + FRANCH to FRANCH, + GERMAN to GERMAN, + JAPANESE to JAPANESE, + CHINESE to CHINESE_SITE_LOCALE, + ) + + fun shareUrl(tokenId: String): String { + val locale = siteLocales[Locale.getDefault().language] ?: ENGLISH + return "$BASE_URL/$locale/$CRYPTOCURRENCIES_PATH/$tokenId" + } +} \ No newline at end of file diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/models/MarketsListItemUM.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/models/MarketsListItemUM.kt index 9d48452aef..7659617208 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/models/MarketsListItemUM.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/models/MarketsListItemUM.kt @@ -22,6 +22,7 @@ data class MarketsListItemUM( val isUnder100kMarketCap: Boolean, val stakingRate: TextReference?, val updateTimestamp: Long?, + val networks: List? = null, ) { val chartType: MarketChartLook.Type = when (trendType) { PriceChangeType.UP -> MarketChartLook.Type.Growing @@ -29,6 +30,13 @@ data class MarketsListItemUM( PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral } + @Immutable + data class Network( + val networkId: String, + val contractAddress: String?, + val decimalCount: Int?, + ) + @Immutable data class Price( val text: String, diff --git a/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/account/AccountPortfolioItemUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt index 6bb24f24e3..150bc43608 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountPortfolioItemUMConverter.kt @@ -48,7 +48,6 @@ class AccountPortfolioItemUMConverter( ) UserWalletItemUM.Information.Loaded(text) } - is Account.Payment -> TODO("[REDACTED_JIRA]") } private fun getImageState(account: Account.CryptoPortfolio) = when (account) { @@ -56,7 +55,6 @@ class AccountPortfolioItemUMConverter( name = account.accountName.toUM().value, icon = CryptoPortfolioIconConverter.convert(account.icon), ) - is Account.Payment -> TODO("[REDACTED_JIRA]") } private fun getBalanceInfo(): UserWalletItemUM.Balance { diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt b/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt index e4575a197c..f45c250868 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt @@ -82,7 +82,7 @@ fun PortfolioSelectRow( @Immutable data class PortfolioSelectUM( - val icon: AccountIconUM.CryptoPortfolio?, + val icon: AccountIconUM?, val name: TextReference, val isAccountMode: Boolean, val isMultiChoice: Boolean, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountAccountConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountAccountConverter.kt index ea09230bd7..d347bf2908 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountAccountConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountAccountConverter.kt @@ -1,5 +1,6 @@ package com.tangem.common.ui.amountScreen.converters +import com.tangem.common.ui.account.AccountIconUM import com.tangem.common.ui.account.AccountTitleUM import com.tangem.common.ui.account.CryptoPortfolioIconConverter import com.tangem.common.ui.account.toUM @@ -11,12 +12,12 @@ class AmountAccountConverter( private val prefixText: TextReference, private val isAccountsMode: Boolean, private val walletTitle: TextReference, -) : Converter { - override fun convert(value: Account.CryptoPortfolio?): AccountTitleUM { +) : Converter { + override fun convert(value: Account?): AccountTitleUM { return if (value != null && isAccountsMode) { AccountTitleUM.Account( name = value.accountName.toUM().value, - icon = CryptoPortfolioIconConverter.convert(value.icon), + icon = getAccountIcon(account = value), prefixText = prefixText, ) } else { @@ -25,4 +26,11 @@ class AmountAccountConverter( ) } } + + private fun getAccountIcon(account: Account): AccountIconUM { + return when (account) { + is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(account.icon) + is Account.Payment -> AccountIconUM.Payment + } + } } \ No newline at end of file diff --git a/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/notifications/Notifications.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt index 68a3f77292..7f8ec96750 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt @@ -4,8 +4,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.ds.message.TangemMessage +import com.tangem.core.ui.ds.message.TangemMessageUM import com.tangem.core.ui.res.TangemTheme import kotlinx.collections.immutable.ImmutableList @@ -44,4 +48,66 @@ fun LazyListScope.notifications( ) }, ) +} + +/** + * Displays a list of notifications using TangemMessage composables. + * + * @param notifications List of NotificationConfig objects to be displayed. + * @param contentColor Color to be used for the content of the notifications. + * @param modifier Optional Modifier for the notifications. + * @param hasPaddingAbove Boolean indicating whether to add padding above the first notification. + */ +fun LazyListScope.notifications2( + notifications: ImmutableList, + contentColor: Color, + modifier: Modifier = Modifier, + hasPaddingAbove: Boolean = false, +) { + itemsIndexed( + items = notifications, + key = { index, item -> item.title?.hashCode()?.plus(index) ?: index }, + contentType = { _, item -> item::class.java }, + itemContent = { i, item -> + val topPadding = if (i == 0 && hasPaddingAbove) 0.dp else 12.dp + TangemMessage( + config = item, + contentColor = contentColor, + modifier = modifier + .padding(top = topPadding) + .animateItem(), + ) + }, + ) +} + +/** + * Displays a list of notifications using TangemMessage composables. + * + * @param notifications List of TangemMessageUM objects to be displayed. + * @param contentColor Color to be used for the content of the notifications. + * @param modifier Optional Modifier for the notifications. + * @param hasPaddingAbove Boolean indicating whether to add padding above the first notification. + */ +fun LazyListScope.notifications( + notifications: ImmutableList, + contentColor: Color, + modifier: Modifier = Modifier, + hasPaddingAbove: Boolean = false, +) { + itemsIndexed( + items = notifications, + key = { _, item -> item.id }, + contentType = { _, item -> item::class.java }, + itemContent = { i, item -> + val topPadding = if (i == 0 && hasPaddingAbove) TangemTheme.dimens2.x0 else TangemTheme.dimens2.x2 + TangemMessage( + messageUM = item, + contentColor = contentColor, + modifier = modifier + .padding(top = topPadding) + .animateItem(), + ) + }, + ) } \ No newline at end of file diff --git a/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/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 083fa85275..ae0d6a4253 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -97,6 +97,7 @@ sealed class AnalyticsParam { data object NewsList : ScreensSources("News List") data object NewsLink : ScreensSources("News Link") data object NewsPage : ScreensSources("News Page") + data object Portfolio : ScreensSources("Portfolio") } sealed class TxSentFrom(val value: String) { @@ -292,6 +293,7 @@ sealed class AnalyticsParam { const val ACCOUNT_DERIVATION = "Account Derivation" const val REFERRAL = "Referral" const val REFERRAL_ID = "Referral_ID" + const val SEARCHED = "Searched" } } diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SwapAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SwapAnalyticsEvent.kt new file mode 100644 index 0000000000..e6e5864f91 --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SwapAnalyticsEvent.kt @@ -0,0 +1,29 @@ +package com.tangem.core.analytics.models.event + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.SEARCHED +import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM +import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources + +/** +[REDACTED_AUTHOR] + */ +sealed class SwapAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent("Swap", event, params) { + + data class TokenSelected( + val token: String, + val source: ScreensSources, + val isSearched: Boolean, + ) : SwapAnalyticsEvent( + event = "Token Selected", + params = mapOf( + TOKEN_PARAM to token, + SOURCE to source.value, + SEARCHED to if (isSearched) "True" else "False", + ), + ) +} \ No newline at end of file diff --git a/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..c4ccee2282 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -11,10 +11,6 @@ "name": "STAKING_TON_ENABLED", "version": "5.28.0" }, - { - "name": "NFT_MEDIA_CONTENT_ENABLED", - "version": "undefined" - }, { "name": "STAKING_CARDANO_ENABLED", "version": "5.31.1" @@ -31,18 +27,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" @@ -77,7 +65,7 @@ }, { "name": "SWAP_MARKET_LIST_ENABLED", - "version": "undefined" + "version": "5.34" }, { "name": "EARN_BLOCK_ENABLED", @@ -86,5 +74,9 @@ { "name": "HOLD_TO_CONFIRM_BUTTON_ENABLED", "version": "undefined" + }, + { + "name": "WALLET_REORDER_FEATURE_ENABLED", + "version": "5.34" } ] diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index 3a4c3d9e84..d4e2600da3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -67,6 +67,7 @@ interface TangemExpressApi { @Query("refundAddress") refundAddress: String?, // for cex only @Query("refundExtraId") refundExtraId: String?, // for cex only @Query("partnerOperationType") partnerOperationType: String?, // swap/ swap-and-send + @Query("toExtraId") toExtraId: String?, // swap-and-send memo ): ApiResponse @GET("exchange-status") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt index 4712dadadc..a8ed2fd5dc 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt @@ -35,6 +35,9 @@ data class ExchangeProvider( @Json(name = "exchangeOnlyWithinSingleAddress") val isExchangeOnlyWithinSingleAddress: Boolean = false, + + @Json(name = "isExtraIdSupported") + val isExtraIdSupported: Boolean = false, ) @JsonClass(generateAdapter = false) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt index 9b96730278..21f255653b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt @@ -24,7 +24,11 @@ data class SwapPairProvider( @Json(name = "rateTypes") val rateTypes: List, -) +) { + fun hasOnlyFixedRateType(): Boolean { + return rateTypes.isNotEmpty() && rateTypes.all { it == RateType.FIXED } + } +} @JsonClass(generateAdapter = false) enum class RateType { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt index eaeb531130..7b666f4171 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/TangemTechMarketsApi.kt @@ -18,6 +18,7 @@ interface TangemTechMarketsApi { @Query("order") order: String, @Query("search") search: String?, @Query("timestamp") timestamp: Long?, + @Query("showNetworks") showNetworks: Boolean? = null, ): ApiResponse @GET("v1/coins/{coin_id}") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt index 31168b8c5b..bd241731d6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/markets/models/response/TokenMarketListResponse.kt @@ -27,8 +27,16 @@ data class TokenMarketListResponse( @Json(name = "is_under_market_cap_limit") val isUnderMarketCapLimit: Boolean?, @Json(name = "staking_opportunities") val stakingOpportunities: List?, @Json(name = "max_yield_apy") val maxYieldApy: BigDecimal?, + @Json(name = "networks") val networks: List? = null, ) { + @JsonClass(generateAdapter = true) + data class Network( + @Json(name = "network_id") val networkId: String, + @Json(name = "contract_address") val contractAddress: String?, + @Json(name = "decimal_count") val decimalCount: Int?, + ) + @JsonClass(generateAdapter = true) data class PriceChangePercentage( @Json(name = "24h") val h24: BigDecimal?, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt index a476b5dc1b..48cc8fc4fa 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt @@ -27,6 +27,7 @@ data class OrderResponse( @Json(name = "emboss_name") val embossName: String?, @Json(name = "product_instance_id") val productInstanceId: String?, @Json(name = "payment_account_id") val paymentAccountId: String?, + @Json(name = "transaction_hash") val transactionHash: String?, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index f9bdbc2aa6..aa8748eb2c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -203,6 +203,20 @@ interface TangemTechApi { * Used when yield operations generate intermediate transactions * that should not trigger notifications. */ - @POST("v1/transaction-events") + @POST("v2/transaction-events") suspend fun transactionEvents(@Body name: TransactionEventBody): ApiResponse + + // region Earn + @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("networkIds") networks: List? = null, + ): ApiResponse + + @GET("v1/earn/networks") + suspend fun getEarnNetworks(@Query("type") type: String? = null): ApiResponse + // endregion } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/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..b123c7d912 --- /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? = null, + @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/api/tangemTech/models/TransactionEventBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/TransactionEventBody.kt index 806b775af1..853a2ce043 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/TransactionEventBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/TransactionEventBody.kt @@ -7,6 +7,7 @@ import com.squareup.moshi.JsonClass data class TransactionEventBody( @Json(name = "transactionId") val transactionId: String, @Json(name = "operationType") val operationType: OperationType, + @Json(name = "userAddress") val userAddress: String? = null, ) @JsonClass(generateAdapter = false) diff --git a/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> - - val userWalletsSync: List - - fun getSyncOrNull(key: UserWalletId): UserWallet? - - fun getSyncStrict(key: UserWalletId): UserWallet - - suspend fun update( - userWalletId: UserWalletId, - update: suspend (UserWallet) -> UserWallet, - ): CompletionResult -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt index 591bc9f6be..25a33b8970 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.local.visa import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.TangemPayWithdrawState import com.tangem.domain.visa.model.TangemPayAuthTokens @Suppress("TooManyFunctions") @@ -28,11 +29,25 @@ interface TangemPayStorage { suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? - suspend fun storeWithdrawOrder(userWalletId: UserWalletId, orderId: String) + /** Called after creating withdraw order, active order id */ + suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) - suspend fun getWithdrawOrderId(userWalletId: UserWalletId): String? + /** Called after creating withdraw order, saves order data */ + suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) - suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) + /** Returns single active order id. Once the order is completed, deletes id from storage. + * Only one active order allowed for a wallet */ + suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? + + /** Returns all withdraw orders saved. + * Once we get tx hash for an order, it gets deleted from this storage */ + suspend fun getWithdrawOrders(userWalletId: UserWalletId): List? + + /** Deletes active withdraw order. Called after order is completed */ + suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) + + /** Deletes withdraw order data. Called after getting its tx hash */ + suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/review/DummyReviewManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/review/DummyReviewManager.kt new file mode 100644 index 0000000000..d418ec6905 --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/review/DummyReviewManager.kt @@ -0,0 +1,5 @@ +package com.tangem.core.navigation.review + +class DummyReviewManager : ReviewManager { + override fun request(onDismissClick: () -> Unit) = Unit +} \ No newline at end of file diff --git a/core/navigation/src/main/java/com/tangem/core/navigation/review/ReviewManager.kt b/core/navigation/src/main/java/com/tangem/core/navigation/review/ReviewManager.kt new file mode 100644 index 0000000000..b7e577b4fa --- /dev/null +++ b/core/navigation/src/main/java/com/tangem/core/navigation/review/ReviewManager.kt @@ -0,0 +1,12 @@ +package com.tangem.core.navigation.review + +/** + * Interface to manage in-app review requests. + */ +interface ReviewManager { + + /** + * Requests an in-app review flow. + */ + fun request(onDismissClick: () -> Unit) +} \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 5ee7d6ff61..4cc165f633 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -1431,6 +1431,8 @@ Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen. Tausche mehr Token zu besseren Kursen direkt in deiner Brieftasche. Neuer Swap-Anbieter verfügbar! + Suchen Sie nach einem beliebigen Token, auch wenn es noch nicht in Ihrer Liste ist. + Nutzen Sie die Suche, um zu finden, was Sie benötigen. Vertraue auf den rund um die Uhr verfügbaren Support bei allen Problemen Immer für Dich da Mehrere vertrauenswürdige Anbieter an einem Ort – tausche mühelos alle Vermögenswerte in Deiner Wallet @@ -1496,7 +1498,7 @@ Ihre Karte ist entsperrt. Abhebung Auf gerooteten Geräten nicht nutzbar. - KYC abbrechen + KYC vom Hauptbildschirm ausblenden Guthaben hinzufügen Aufladeoptionen Kartennummer @@ -1562,10 +1564,10 @@ Status anzeigen KYC für Tangem Pay in Arbeit Dokumente werden in der Regel innerhalb von 5 Minuten automatisch überprüft. In seltenen Fällen, wenn eine manuelle Überprüfung erforderlich ist, kann es bis zu 48 Stunden dauern. - KYC abgelehnt + Abgelehnt KYC-Sperre ausblenden - Entschuldigung, wir konnten dies nicht überprüfen. - Dein Profil. + Leider konnten wir Ihre Identität + nicht verifizieren. Holen Sie sich Ihre kostenlose virtuelle Tangem Visa-Karte Nutzen Sie USDC für alltägliche Zahlungen Karte erhalten @@ -1888,6 +1890,8 @@ Service vorübergehend nicht verfügbar Die Menge der zu tauschenden Token darf folgende Werte nicht überschreiten %s Der zu tauschende Betrag muss mindestens %s betragen + Der Tausch ist für dieses Paar nicht verfügbar. Bitte wählen Sie ein anderes Token und versuchen Sie es erneut. + Nicht unterstütztes Swap-Paar. Bitte änder den zu tauschenden Betrag Bei dieser Karte oder Ring kann es sich um ein Produktionsmuster oder eine Fälschung handeln Echtheitsprüfung fehlgeschlagen diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index fac79c1d1f..0ec38e3506 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -4,7 +4,7 @@ Saltar de todos modos Código de acceso no establecido Cambiar código - Su código de acceso se utilizará para desbloquear su billetera y proteger el acceso a sus activos. + Su código de acceso desbloquea y protege el acceso a su billetera Usar de todos modos Este código de acceso se puede adivinar fácilmente. Introduzca el código de acceso @@ -399,6 +399,7 @@ Termine el staking Debido a limitaciones sobre %1$s, solo %2$d UTXO pueden caber en una sola transacción. Esto significa que solo puedes enviar %3$s o menos. Debe reducir la cantidad. Valor copiado + Billeteras semana con @@ -468,9 +469,15 @@ Red de %s Envíe fondos utilizando solo Las mejores oportunidades + Limpiar filtro + La lista está temporalmente vacía porque se está actualizando. Vuelva a consultarla en un momento. Todas las redes Todos los tipos + Filtrar por + Mis redes + Redes Mayormente usado + Sin resultados Ganar Hola equipo de soporte, he encontrado un error con el código: %s Error de WalletConnect @@ -543,7 +550,7 @@ Comprando %s Comprando %s... Ocultar esta transacción - Una vez oculto, el estado de la transacción no se puede volver a ver. En su lugar, basta con deslizar el dedo para descartarla. + Si oculta esta transacción, ya no aparecerá en la pantalla de estado. Si solo quieres cerrar la pantalla de estado y volver más tarde, solo tiene que deslizar el dedo hacia otro lado. ¿Ocultar el estado de la transacción? Este token no es compatible. Por favor, elige un token diferente para intercambiar. %s no está soportado @@ -552,9 +559,9 @@ ID : %s ID de transacción copiado Convierta una de sus activos de su portafolio por este token - Una mayor velocidad significa una confirmación más rápida y una tarifa de red más alta. %s + Una mayor velocidad significa una confirmación más rápida\ny una tarifa de red más alta. %s Elija la velocidad - Elija qué token utilizar para pagar la tarifa de red. %s + Elige qué token utilizar para pagar\nla tarifa de red. %s Elija token Mercado y noticias De actualidad @@ -577,7 +584,7 @@ Ocurrió un error. Código: %s Requiere memo Esta transacción - Especifique el límite aprobado para el token seleccionado. + Al aprobar, permite que el contrato inteligente utilice sus tokens en transacciones futuras. Montante %s La función Aprobar es necesaria para otorgar permiso a otra dirección para usar una cantidad específica de sus tokens. Por diseño, los contratos inteligentes no pueden acceder a sus tokens sin su aprobación. Al \"desbloquear\" sus tokens, autoriza al contrato inteligente de StakeKit a usarlos. Los mineros de la red reciben una tarifa de gas (pagada por usted) por registrar esta acción en la cadena de bloques. Puede apostar su token después de dar su aprobación. Para continuar, debe autorizar el contrato inteligente de Polygon para utilizar su %s @@ -733,7 +740,7 @@ Sin datos Análisis del Mercado Acciones rápidas - Busque en el mercado + Buscar tokens Resultado Ver tokens con marketcap inferior a 100.000$ Mostrar tokens @@ -748,10 +755,10 @@ 7d Todo Compradores experimentados - Evaluación + Capitalización de mercado Ordenar por - Top Ganadores - Top Perdedores + Ganadores + Perdedores Tendencias Modo Rendimiento Staking es la forma más fácil de recibir recompensas por sus criptomonedas. %s @@ -801,7 +808,7 @@ Capital. de mercado El valor total de una criptomoneda calculado multiplicando su precio por la cantidad de monedas en circulación Capital. de mercado - Evaluación de mercado + Posición en el mercado Posición en la clasificación de criptomonedas entre todas las monedas según la capitalización de mercado Evaluación de mercado Suministro máximo @@ -823,6 +830,7 @@ Volumen Tire hacia arriba o toque la barra de búsqueda para agregar tokens directamente desde el mercado Agregar tokens + Añadir más tokens Potencie sus activos mientras los suministra con acceso inmediato. %s Activar Modo Rendimiento Debes actualizar a %1$s para crear una mobile wallet @@ -840,7 +848,7 @@ Resumen rápido Noticias Tokens relacionados - Fuentes + Noticias relacionadas Manténgase informado NFC no está disponible en su dispositivo Acerca de NFT @@ -927,9 +935,9 @@ Por favor repita la operación. La tarjeta/anillo se restablecerá a la configuración de fábrica. Error de activación Agregar tokens - Ha agregado una tarjeta/anillo de backup. Cuando el proceso de backup finalice, no podrá añadir más dispositivos de backup. Si tiene otra tarjeta o anillo, agréguela al backup. ¿Quiere continuar el proceso de backup? - El proceso de backup está parcialmente completo. No puede salir ahora. - La frase de contraseña es una característica de seguridad avanzada utilizada por las billeteras criptográficas. Agrega una palabra o frase adicional de su elección a su frase de recuperación ya existente para desbloquear un conjunto completamente nuevo de direcciones. + Ha añadido una tarjeta o anillo como copia de seguridad. Una vez finalizada la copia de seguridad, no puedes añadir más dispositivos. Si tiene una tarjeta o anillo más, añádalo ahora. ¿Quiere continuar? + La copia de seguridad está parcialmente completa y no se puede salir ahora. + Una frase de contraseña es una función de seguridad opcional que añade una palabra o frase a su frase de recuperación, creando un nuevo conjunto de direcciones de billetera para una mayor protección. Agregar una tarjeta o un anillo de backup Escanee la tarjeta Escanee la tarjeta núm. %d @@ -939,7 +947,7 @@ Continuar a mi billetera Finalizar el backup Recibir cripto - Escanee la tarjeta principal + Escanee la tarjeta principal o anillo Finalizar más tarde ¿Cómo funciona? Vamos a generar todas las claves en su tarjeta o anillos y crear una billetera segura @@ -993,7 +1001,7 @@ Ningunos dispositivos de backup Notificaciones Un dispositivo de backup agregado - Prepare su tarjeta + Prepare su tarjeta o anillo Dos dispositivos de backup agregados Para empezar, simplemente recargue la billetera con cualquier cantidad Para empezar, simplemente recargue la billetera con más de %1$s %2$s @@ -1028,7 +1036,7 @@ El monto de la compra no debe ser mayor a %s La cantidad a comprar debe ser como mínimo %s No hay proveedores disponibles para esta moneda - Más rápido + Procesamiento más rápido Pagar con Método de pago Disponible hasta %s @@ -1299,7 +1307,7 @@ Tasa de recompensa promedio ¿Cómo funciona el staking? %s beneficio estimado - Calificación de mercado + Posición en el mercado Métrica Según las reglas de la red %1$s, se pueden reclamar recompensas desde %2$s. Las cantidades a continuación se acreditarán en su cuenta al deshacer el staking. Mínimo requerido @@ -1412,7 +1420,7 @@ Sus stakes Almacene sus criptomonedas de forma segura manteniendo las claves privadas almacenadas en su tarjeta Billetera de hardware revolucionaria - Hasta 3 tarjetas físicas o anillos por billetera + Añade hasta 3 tarjetas o anillos a una billetera Backup ultra seguro Una billetera de hardware para su Bitcoin, Ethereum y muchas más monedas simultáneamente – todo en una sola tarjeta o anillo Miles de monedas @@ -1428,6 +1436,9 @@ La red cobrará una tasa de aprobación del token para verificar que usted autoriza el uso de su token para el intercambio. Intercambie más tokens a mejores tasas directamente en su billetera. ¡Nuevo proveedor de intercambio disponible! + ¿Busca algo más?\n¡Intente buscar o explorar otra criptomoneda! + Busque cualquier token, incluso si aún no está en su lista. + Utilice la búsqueda para encontrar lo que necesite Siéntase seguro con una asistencia permanente que le ayudará con cualquier problema Siempre aquí Múltiples proveedores de confianza en un solo lugar: intercambie cualquier activo sin esfuerzo en su billetera @@ -1493,7 +1504,7 @@ Tu tarjeta está descongelada. Retirada No se puede usar en un dispositivo rooteado - Cancelar KYC + Ocultar verificación de la pantalla Agregar fondos Opciones de recarga Número de tarjeta @@ -1554,15 +1565,15 @@ ¿Seguro que desea detener el proceso KYC? Puede retomarlo en cualquier momento. No pudimos verificar tu perfil. Si tienes alguna pregunta, contacta con el soporte. Lamentablemente, no pudimos verificar tu identidad - El proceso KYC ha fallado + KYC rechazado KYC en curso Ver estado KYC en progreso para Tangem Pay Los documentos suelen verificarse automáticamente en menos de 5 minutos. En casos excepcionales que requieran revisión manual, el proceso puede tardar hasta 48 horas. - KYC rechazado + Rechazado Ocultar el bloque KYC - Lo sentimos, no pudimos verificarle - su perfil. + Lo sentimos, no pudimos verificar + u identidad. Obtén tu tarjeta virtual Tangem Visa gratuita Usa USDC para pagos cotidianos Obtener tarjeta @@ -1621,7 +1632,7 @@ Cambie este token por otro por una tarifa de servicio de %1$s del %2$s al %3$s de febrero. Intercambie con Changelly, %s comisiones Intercambie ahora - Criptomonedas populares 🔥 + Tendencia del mercado 🔥 No disponible para compra No disponible para vender No disponible para cambio desde %s @@ -1670,7 +1681,7 @@ ¿Quiere utilizar\nnotificaciones push? Active las notificaciones push y le avisaremos al instante cuando le lleguen fondos. No se pierda ninguna transacción - Agregar una nueva billetera + Agregar billetera Si olvida esta billetera sin una copia de seguridad, perderá permanentemente el acceso a sus fondos. ¿Estás seguro de que deseas olvidar esta billetera? Ha ocurrido un error, por favor escanee su tarjeta o anillo para iniciar sesión @@ -1748,7 +1759,7 @@ Desbloquear Escanee su tarjeta para desbloquear el acceso Desbloqueo necesario - Elija cómo agregar su billetera + Elige tu tipo de billetera Escanee su tarjeta o anillo Tangem para restaurarlo o importarlo desde otra billetera. Crear una billetera de hardware ¿Quiere comprar una  Billetera Tangem? @@ -1838,7 +1849,7 @@ Use %s o escanee una tarjeta/anillo para desbloquear el acceso a su billetera El proceso de obtención de permisos está actualmente en marcha y se completará pronto. Aprobación en curso - Parece que la activación de la tarjeta no ha funcionado correctamente. Esto puede deberse a un problema con el módulo NFC de su dispositivo o a una mala conexión de la tarjeta con su dispositivo. Comuníquese con nuestro equipo de soporte para obtener ayuda. + La activación no se completó correctamente. Esto podría deberse a un problema de NFC o a una lectura incorrecta. Contacte con nuestro equipo de soporte para obtener ayuda. Error de activación El 3 de diciembre de 2024, la red BEP-2 fue desactivada por decisión de los desarrolladores de la red y ya no es compatible BNB Beacon Chain se cerrará @@ -1851,6 +1862,7 @@ Actualizar Iniciar migración Copiar + Para mantener el acceso a sus fondos, comience la migración de acuerdo con las pautas oficiales de Clore. La firma de mensajes no es compatible con esta red No se puede firmar el mensaje. Por favor, inténtelo de nuevo. Según la documentación oficial de Clore, todas las monedas recibidas antes del 21 de diciembre serán migradas a Clore (token ERC-20); las monedas recibidas después de esa fecha no lo serán. Se está desarrollando una solución de transferencia — mantente atento. @@ -1873,7 +1885,7 @@ Aprobación en proceso El monto mínimo de transacción es %1$s. Asegúrese de que el saldo restante después del canje no sea inferior a %2$s. No tienes tokens en tu portafolio a los que puedas intercambiar %s. Por favor, añade otro token para realizar el intercambio. - No hay tokens disponibles para intercambiar + No se agregaron tokens compatibles Para realizar una transacción necesita depositar %1$s %2$s No se pueden cubrir %s tarifa El monto a recibir debe ser de al menos %s @@ -1884,6 +1896,8 @@ Servicio no disponible temporalmente La cantidad de tokens a intercambiar no debe exceder %s El monto a cambiar debe ser de al menos %s + El intercambio no está disponible para este par. Por favor, seleccione otro token e inténtelo de nuevo. + Par de intercambio no compatible. Por favor cambie la cantidad a cambiar Esta tarjeta podría ser una muestra de producción o una falsificación La verificación de autenticidad falló @@ -2045,11 +2059,11 @@ Crear o importar una billetera de software Cree o importe una billetera de software en su teléfono. Empezar con Mobile Wallet - Otro método + Otros métodos Utilice la billetera de hardware Tangem Obtenga más información y compre Ignorar - Tiene un backup interrumpido. ¿Quiere reanudarlo? + Su copia de seguridad se ha interrumpido. ¿Desea reanudarla? Sí, reanudar Descartar Si descarta el backup ahora, tendrá que restablecer los dispositivos a los ajustes de fábrica para empezar de nuevo diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index a89fafa94f..41913d6515 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1434,6 +1434,8 @@ Le réseau facturera des frais d\'approbation de jeton pour vérifier que vous autorisez l\'utilisation de votre jeton pour l\'échange. Échangez plus de jetons à de meilleurs taux directement dans votre portefeuille. Nouveau fournisseur d\'échange disponible ! + Recherchez n’importe quel token, même s’il ne figure pas encore dans votre liste. + Utilisez la recherche pour trouver ce dont vous avez besoin. Ayez confiance en notre assistance 24 heures sur 24 pour vous aider à résoudre tous vos problèmes Assistance 24 heures sur 24 Plusieurs fournisseurs de confiance en un seul endroit : échangez n\'importe quel actif facilement @@ -1499,7 +1501,7 @@ Votre carte est dégelée. Retrait Impossible à utiliser sur un appareil rooté - Annuler KYC + Masquer la vérification de l\'écran Ajouter des fonds Options de recharge Numéro de carte @@ -1565,10 +1567,10 @@ Voir le statut KYC en cours pour Tangem Pay Les documents sont généralement vérifiés automatiquement en moins de 5 minutes. Dans de rares cas nécessitant une vérification manuelle, le processus peut prendre jusqu’à 48 heures. - KYC rejeté + Refusé Masquer le bloc KYC - Désolé, nous n\'avons pas pu vérifier. - votre profil. + Désolé, nous n\'avons pas pu vérifier + votre identité. Obtenez votre carte virtuelle Tangem Visa gratuite Utilisez USDC pour les paiements quotidiens Obtenir la carte @@ -1891,6 +1893,8 @@ Service temporairement indisponible Le nombre de jetons à échanger ne doit pas dépasser %s Le montant à échanger doit être d\'au moins %s + L’échange n’est pas disponible pour cette paire. Veuillez sélectionner un autre token et réessayer. + Paire d’échange non prise en charge. Veuillez modifier le montant à échanger Cette carte pourrait être un échantillon de production ou une contrefaçon Échec de la vérification d\'authenticité diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 50290eaea8..9cd39c1c7a 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -98,6 +98,7 @@ Impossibile sbloccare la carta. Riprova più tardi. La tua carta è sbloccata. Prelievo + Nascondi verifica dalla schermata Aggiungi fondi Opzioni di ricarica Numero carta @@ -151,10 +152,14 @@ Tangem Pay Non siamo riusciti a verificare il tuo profilo. Per domande, contatta il supporto. Purtroppo non siamo riusciti a verificare la tua identità + KYC rifiutato KYC in corso Visualizza stato KYC in corso per Tangem Pay I documenti vengono solitamente verificati automaticamente entro 5 minuti. In rari casi, se è necessaria una revisione manuale, il processo può richiedere fino a 48 ore. + Rifiutato + Spiacenti, non siamo riusciti a verificare + la tua identità. Ottieni la tua carta virtuale Tangem Visa gratuita Usa USDC per i pagamenti quotidiani Ottieni carta diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index d554a4bd2f..6b87995090 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) のみを送信してください。他のトークンやネットワークを使用すると、資金を失う可能性があります。 デフォルト レガシー - デバイスの生体認証をリセットするか、サポートにお問い合わせください + 生体認証で問題が発生しました。端末の生体認証をリセットするか、サポートにお問い合わせください。 認証エラー スキャン方法 サポートをリクエストする @@ -392,6 +392,7 @@ ステーキング解除 %1$sの制限により、1つのトランザクションに収まるUTXOは%2$d個のみです。つまり、 %3$s以下しか送信できません。量を減らす必要があります。 値がコピーされました + ウォレット はい @@ -461,9 +462,15 @@ %sネットワーク 下記のみを使用して資金を送金する おすすめ + 絞り込みを解除 + リストは現在更新中のため、一時的に空になっています。しばらくしてからご確認ください。 すべてのネットワーク すべての種類 + 絞り込み + マイネットワーク + ネットワーク よく使われています + 該当する結果はありません 運用 こんにちは、サポートチームの皆さん、コード %s のエラーが発生しました。 WalletConnectエラー @@ -814,6 +821,7 @@ 取引量 これをドラッグするか、検索窓をタップして、マーケットから直接トークンを追加します トークンを追加 + トークンを追加 資産を常時アクセス可能な状態に保ったまま、パワーアップさせよう。%s 利息モードを有効にする モバイルウォレットを作成するには、%1$sにアップデートする必要があります @@ -827,7 +835,7 @@ %d分前 クイックまとめ - 関連ニュース + ニュース 関連トークン 関連ニュース 最新情報を入手 @@ -1409,6 +1417,9 @@ ネットワークは、あなたがトークンのスワップを承認していることを確認するために、トークン承認手数料を請求します。 より多くのトークンをより良いレートで、ウォレット内にて直接交換します。 新しいスワッププロバイダーが利用可能になりました! + 他のものをお探しですか?\n検索してみるか、別の暗号資産をチェックしてみましょう! + どのトークンでも検索できます。まだ一覧に表示されていないものでも検索可能です。 + 必要なものは検索して見つけましょう。 24時間体制のサポートであらゆる問題に対応します。 いつもここに 複数の信頼できるプロバイダーが一箇所に集結。ウォレット内で様々な暗号資産を簡単に交換できます。 @@ -1474,7 +1485,7 @@ カードの凍結が解除されました 出金 Root化された端末では使用できません - KYCをキャンセル + メイン画面からKYCを非表示にする 資金を追加 入金オプション カード番号 @@ -1535,15 +1546,15 @@ KYC手続きを中止しますか?いつでも再開できます。 プロフィールを確認できませんでした。ご不明な点があればサポートまでお問い合わせください。 申し訳ございませんが、本人確認を行うことができませんでした - KYCの認証に失敗しました + KYCが承認されませんでした KYC進行中 ステータスを表示 Tangem PayのKYC手続き進行中 書類は通常、5分以内に自動で確認されます。手動での審査が必要な稀な場合は、最大48時間かかることがあります。 - KYCが承認されませんでした + 拒否されました KYCブロックを非表示 - 申し訳ありません、確認できませんでした - あなたのプロフィール。 + 申し訳ございませんが、 + 本人確認ができませんでした。 無料のTangem Visaバーチャルカードを入手 日常の支払いにUSDCを利用 カードをGET diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 550f256f3f..350f3c2c43 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -31,7 +31,7 @@ Этот аккаунт не может быть архивирован. Мы не смогли создать аккаунт. Пожалуйста, попробуйте позже. Аккаунт создан - Архивный аккаунт + Архивировать аккаунт Архив Вы архивируете свой аккаунт, но в любое время можете вернуть его обратно Архивация... @@ -412,6 +412,7 @@ Завершить стейкинг Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. Значение скопировано + Кошельки неделю с Да @@ -482,6 +483,7 @@ Отправляйте средства, используя только Лучшие возможности Очистить фильтр + Список временно пуст — он обновляется. Пожалуйста, зайдите чуть позже. Все сети Все типы Фильтровать по @@ -1466,6 +1468,8 @@ Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. Обменивайте больше токенов по лучшим курсам прямо в вашем кошельке. Новый провайдер обмена! + Найдите любой токен, даже если его ещё нет в вашем списке + Используйте поиск, чтобы найти то, что вам нужно. Чувствуйте уверенность с круглосуточной поддержкой, готовой помочь в любой ситуации! Круглосуточная поддержка Надежные провайдеры в одном месте — обменивайте любые активы легко и быстро прямо в своем кошельке! @@ -1511,6 +1515,8 @@ Заморозить Карта заморожена Обратиться в поддержку + %s・%s + MCC %s Другое Невозможно использовать на устройствах с root-доступом. Успешно завершено @@ -1528,7 +1534,7 @@ Карта разморожена Вывести Запрещено использовать на root-устройствах - Отменить KYC + Скрыть KYC с главной Пополнить Способы пополнения Номер @@ -1588,11 +1594,15 @@ Если вы отмените KYC сейчас, вы сможете потом начать его заново Не удалось подтвердить ваш профиль. Если у вас есть вопросы, обратитесь в службу поддержки. К сожалению, нам не удалось подтвердить вашу личность + KYC отклонено KYC в процессе Посмотреть статус KYC в процессе для Tangem Pay Обычно документы проверяются автоматически в течение 5 минут, в редких случах при необходимости ручной проверки – до 48 часов. - KYC отклонено + Отклонено + Скрыть KYC с главной + Извините, мы не смогли подтвердить + ваш профиль. Откройте бесплатную виртуальную карту Tangem Visa Оплачивайте ежедневные покупки в USDC Открыть карту @@ -1854,6 +1864,8 @@ Сервис временно недоступен Сумма для обмена должна быть не более %s Сумма для обмена должна быть не менее %s + Обмен для этой пары недоступен. Пожалуйста, выберите другой токен и попробуйте снова. + Неподдерживаемая для обмена пара Пожалуйста, измените сумму для обмена Возможно, данная карта является образцом или подделкой Ошибка проверки подлинности diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index cd70c14fcd..297d8310dc 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 + Натисніть і утримуйте умови участі Умовами використання До @@ -409,6 +413,7 @@ Скасувати стейкінг Через обмеження %1$s в одну транзакцію може поміститися тільки %2$d UTXO. Це означає, що ви можете відправити тільки %3$s або менше. Вам потрібно зменшити суму. Скопійовано + Гаманці тиждень з Так @@ -453,6 +458,7 @@ Цей механізм захищає картку або кільце від безконтактних атак. Між скануванням картки та виконанням команди буде додана затримка. Пароль Перед виконанням будь-якої команди, що тягне за собою зміну стану картки, вам необхідно буде ввести пароль. + Оновлення до апаратного гаманця NFT Реферальна програма Переверніть екран пристрою вниз, щоб швидко приховати та відобразити баланси @@ -466,6 +472,7 @@ Підписано Надіслати відгук Деталі + Ви можете мати лише один мобільний гаманець. Оновіть його до апаратного гаманця Tangem або додайте новий апаратний гаманець. Перевірте підключення до інтернету або змініть мережу Умови використання Основна адреса @@ -475,6 +482,17 @@ Надсилання активів в інші мережі призведе до безповоротної втрати. %s мережа Надсилайте кошти, використовуючи лише + Найкращі можливості + Очистити фільтр + Список тимчасово порожній, оскільки триває оновлення. Спробуйте ще раз за мить. + Всі мережі + Всі типи + Фільтрувати за + Мої мережі + Мережі + Часто використовані + Немає результатів + Заробляйте Привіт, команда підтримки, я зіткнувся з помилкою з кодом: %s Помилка WalletConnect Ви використали картку або кільце від іншого гаманця. Прикладіть картку або кільце, пов\'язану з цим гаманцем. @@ -555,6 +573,10 @@ ID: %s ID транзакції скопійовано Обміняйте будь-який актив у своєму портфелі на цей токен + Більш вища швидкість означає більш швидше підтвердження, але і більш вищу мережеву комісію. %s + Обрати швидкість + Оберіть, який токен буде використовуватися для оплати мережевої комісії. %s + Обрати токен Ринок та Новини В тренді Інформація нижче не є обов\'язковою. Ви можете стерти її, якщо бажаєте. @@ -570,6 +592,8 @@ Звернення в підтримку Tangem Не вдається відправити транзакцію Помилка в описі монети + Недостатньо коштів + Комісія за транзакцію Виникла помилка Виникла помилка. Код: %s. Вимагається memo @@ -619,15 +643,20 @@ Спочатку завершіть резервне копіювання Не завершено Інші методи + Збережіть фразу відновлення в безпечному місці та тримайте її в таємниці, щоб захистити свої кошти, а також налаштуйте код доступу для додаткової безпеки. Збережіть фразу відновлення у безпечному місці і тримайте її у таємниці. Фраза відновлення Щоб захистити свій гаманець за допомогою коду доступу, завершіть процес резервного копіювання. Щоб покращити гаманець до апаратного, спочатку створіть резервну копію. + Оновіть свій мобільний гаманець до апаратного гаманця Tangem для найвищого рівня безпеки. Імпортуйте існуючий гаманець або створіть новий. + Оновлення до холодного гаманця Ваші приватні ключі надійно зашифровані та зберігаються на вашому телефоні Ключі зберігаються у застосунку Створіть або відновіть свій гаманець за допомогою вашої фрази відновлення. Резервна копія Мобільний гаманець + Перемістіть свій мобільний гаманець на Tangem картку або кільце у будь який час. + Оновлення до апаратного гаманця Імпортувати існуючий гаманець Ця фраза відновлення вже була імпортована Мобільний гаманець @@ -821,11 +850,13 @@ Обсяг Потягніть вгору або торкніться панелі пошуку, щоб додати токени безпосередньо з маркету Додати токени + Додати токени Збільшуйте дохід з активів, зберігаючи миттєвий доступ до них. %s Активувати режим дохідності Оновіться до версії %1$s, щоб створити мобільний гаманець Мобільний гаманець потребує %1$s або новіше. Всі новини + Вподобайка %d годину тому %d години тому @@ -838,7 +869,10 @@ %d хвилин тому %d хвилин тому + Короткий огляд + Новини Пов\'язані токени + Схожі новини Залишайтеся в курсі подій Функція NFC недоступна на вашому пристрої Про NFT @@ -1017,6 +1051,7 @@ Відновлення коду доступу Ідентичні картки Код доступу + Ви можете мати лише один мобільний гаманець. Оновіть його до апаратного гаманця Tangem або додайте новий апаратний гаманець. Усі пропозиції Доступно з %s Tangem забезпечує доступ к покупці через сторонніх провайдерів згідно з їхніми умовами @@ -1131,6 +1166,7 @@ Скинути картку Я розумію, що після виконання цієї дії у мене більше не буде доступу до поточного гаманця Я розумію, що не можу використати цю картку для відновлення свого коду доступу на інших картках поточного гаманця + Я розумію, що повністю втрачу доступ до своєї картки Tangem Pay та всіх коштів на ній без можливості відновлення. Скидання до заводських налаштувань призведе до повного видалення гаманця з обраної картки або кільця. Ви не зможете відновити поточний гаманець або використати цю картку або кільце для відновлення коду доступу. Скидання до заводських налаштувань призведе до повного видалення гаманця з обраної картки або кільця. Ви не зможете відновити поточний гаманець. Усі пристрої Tangem було скинуто. @@ -1279,6 +1315,11 @@ Підготуйтеся до сканування кільця або картки, яку ви хочете налаштувати. Забути гаманець Це призведе до видалення гаманця з застосунку. Сам гаманець можна додати знову. + Простий у використанні + Зберігає ваші криптовалюти в безпеці та офлайн. Тонкий, як кредитна картка, безпечніший за банківське сховище. + Без seed-фрази + Найкращий у своєму класі + Холодний гаманець Tangem Ім\'я Змусьте свій токен працювати Мережева комісія — це невелика оплата, необхідна для обробки та підтвердження вашої транзакції в блокчейні. @@ -1290,6 +1331,7 @@ Сума стейкінгу буде округлена до %1$s TRX відповідно до правил мережі. Сума зняття зі стейкінгу буде округлена до %1$s TRX через мережеві правила. APR %1$s%% + Ваші винагороди за стейкінг почнуть надходити через 5 епох (~25 днів), поки ваша делегація реєструється та враховується мережею. Зняти кошти Комісія за стейкінг-акаунт Стейкінг-акаунт — це спеціальний рахунок, на якому зберігаються застейкані SOL токени. Він створюється, коли ви делегуєте свої токени валідатору для участі у перевірці транзакцій та отримання винагород. За створення стейкінг-акаунту стягується невелика комісія, яка повертається після завершення стейкінгу. @@ -1300,6 +1342,7 @@ APR APY Винагороди автоматично накопичуються на вашому балансі щодня. + Винагороди реінвестуються у ваш баланс стейкінгу. Зароблено коштів: %s Доступно Середня ставка винагороди Що таке стейкінг? @@ -1433,6 +1476,9 @@ Мережа стягує комісію за схвалення токену за підтвердження, що саме ви дозволяєте використовувати ваш токен для обміну. Обмінюйте більше токенів за вигіднішим курсом прямо у своєму гаманці. З\'явився новий провайдер обмінів! + Шукаєте щось інше?\nСпробуйте пошукати або перегляньте інші криптовалюти! + Шукайте будь-який токен, навіть якщо його ще немає у вашому списку. + Використовуйте пошук, щоб знайти потрібне Надійна підтримка 24/7, щоб ваші фінансові операції були швидкими та надійними! Цілодобова підтримка Надійні провайдери дозволяють легко обмінювати активи, забезпечуючи повну безпеку у вашому гаманці @@ -1444,6 +1490,7 @@ Просто та зручно — обмін токенів в декілька дотиків Легше, ніж будь-коли Обмін через провайдера + Ваші активи Сума включає: \n• комісію постачальника послуг\n• комісію мережі за відправлення %s з біржі назад на адресу користувача. У суму входить:\n- комісія провайдера\n- мережева комісія за відправку %1$s з біржі назад на адресу користувача. \n\nПроскакування провайдера становить до %2$s Сума включає комісію постачальника послуг. @@ -1458,6 +1505,7 @@ Недостатньо коштів Надати дозвіл Обміняти + Обмін... Ви отримаєте Оберіть токен недоступно @@ -1476,7 +1524,11 @@ Заморозити Вашу картку заморожено. Звернутися до підтримки + Причина: %s + %s.%s + MCC %s Інше + Неможливо використовувати на пристроях з root-правами. Завершено Відхилено В очікуванні @@ -1492,7 +1544,7 @@ Картку розморожено. Виведення коштів Заборонено використовувати на root-пристроях - Скасувати KYC + Приховати KYC з головного екрана Поповнити рахунок Варіанти поповнення Номер картки @@ -1529,6 +1581,7 @@ Обміняйте будь-який актив у вашому портфелі на картку Реквізити картки Розморозити картку + Поверніться до додатка, якщо ви забудете його. Ваш ПІН Вивести Вивід наразі недоступний @@ -1552,10 +1605,15 @@ Ви впевнені, що хочете зупинити процес KYC? Ви можете повернутися до нього в будь-який момент. Не вдалося підтвердити ваш профіль. Якщо у вас є питання, зверніться у службу підтримки. На жаль, нам не вдалося підтвердити вашу особу + KYC відхилено KYC в процесі Переглянути статус Перевірка KYC для Tangem Pay в процесі Документи зазвичай перевіряються автоматично протягом 5 хвилин. У рідкісних випадках, коли потрібна ручна перевірка, це може зайняти до 48 годин. + Відхилено + Приховати блок KYC + Вибачте, ми не змогли підтвердити + вашу особу. Отримайте безкоштовну віртуальну картку Tangem Visa Використовуйте USDC для щоденних платежів Отримати картку @@ -1652,6 +1710,7 @@ Виникла помилка. Код помилки: %s. Спробуйте, будь ласка, знову. Якщо проблема знову виникає — зверніться до нашої служби підтримки. Використовуйте %s або відскануйте картку/кільце, щоб отримати доступ до свого гаманця Не вдалося встановити з\'єднання: Цей dApp використовує Wallet Connect версії 1.0, яка не підтримується. Будь ласка, переконайтеся, що dApp підтримує Wallet Connect версії 2.0 для успішного підключення. + Оновлення до апаратного гаманця Будьте в курсі останніх функцій та новин Миттєві сповіщення про транзакції, обміни та важливі оновлення. Сповіщення про транзакції @@ -1799,7 +1858,18 @@ Зрозуміло! Дуже круто! Оновити + Почати міграцію + Копіювати + Щоб зберегти доступ до своїх коштів, розпочніть міграцію згідно з офіційними рекомендаціями Clore. + Підписання повідомлень не підтримується в цій мережі + Неможливо підписати повідомлення. Будь ласка, спробуйте пізніше. Згідно з офіційною документацією Clore, усі монети, отримані до 21 грудня, будуть мігровані в токен Clore (ERC-20); монети, отримані після цієї дати, — ні. Рішення для переказу перебуває в розробці — стежте за оновленнями. + Повідомлення + Відкрити портал клейму + Щоб зберегти доступ до своїх активів, почніть міграцію у відповідності до офіційної інструкції Clore. + Міграція мережі Clore + Підписати + Підпис Міграція мережі Clore Ви перебуваєте в демонстраційному режимі Демонстраційний режим активовано @@ -1824,6 +1894,8 @@ Сервіс тимчасово недоступний Сума до обміну не повинна перевищувати %s Сума для обміну має бути не менше %s + Обмін для цієї пари недоступний. Будь ласка, виберіть інший токен і спробуйте знову. + Непідтримувана пара для обміну. Будь ласка, змініть суму для обміну Ця картка може бути виробничим зразком або підробкою Перевірка автентичності не вдалася @@ -1981,6 +2053,7 @@ Швидка доставка Почніть в один клік Просто та надійно + Без seed-фрази Простий у використанні Створіть апаратний гаманець з Tangem. Тонкий, як банківська картка, надійний, як банківське сховище. Створіть або імпортуйте програмний гаманець diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index f2a8057772..c410e833bc 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -341,6 +341,7 @@ 無法解凍卡片。請稍後再試。 您的卡片已解凍。 提現 + 在主畫面隱藏身份驗證 添加资金 充值选项 卡號 @@ -394,10 +395,14 @@ Tangem Pay 我们无法验证您的资料。如有任何疑问,请联系客服。 很抱歉,我们无法验证您的身份 + KYC 被拒絕 KYC进行中 查看状态 Tangem Pay 的 KYC 正在進行中 文件通常會在 5 分鐘內自動驗證完成。如需人工審核的特殊情況,可能需要最長 48 小時。 + 已拒絕 + 抱歉,我們無法驗證 + 您的身份 獲取您的免費 Tangem Visa 虛擬卡 使用 USDC 進行日常支付 获取卡片 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 38fbaf5e44..f40c33cb35 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -399,6 +399,7 @@ Unstake Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. Value copied + Wallets week with Yes @@ -1436,6 +1437,9 @@ The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. Exchange more tokens at better rates directly in your wallet. New Swap Provider Available! + Looking for something else?\nTry searching or explore another crypto! + Search for any token, even if it’s not in your list yet. + Use search to find what you need Feel confident with round-the-clock support to help with any issues Always Here Multiple trusted providers in one place—swap any asset effortlessly in your wallet @@ -1501,7 +1505,7 @@ Your card is unfrozen. Withdrawal Unable to use on rooted device - Cancel KYC + Hide KYC from main screen Add funds Top-up options Card Number @@ -1562,12 +1566,12 @@ Are you sure you want to stop the KYC process? You can return to it anytime. We could not verify your profile. If you have any questions, please contact support. Unfortunately, we couldn\'t verify your identity - KYC has failed + KYC rejected KYC in progress View Status KYC in progress for Tangem Pay Documents are usually verified automatically within 5 minutes. In rare cases, if manual review is required, it may take up to 48 hours. - KYC rejected + Rejected Hide KYC block Sorry, we couldn\'t verify your profile. @@ -1893,6 +1897,8 @@ Service temporarily unavailable The amount of tokens to be swapped must not exceed %s The amount to swap must be at least %s + Swapping isn\'t available for this pair. Please select a different token and try again. + Unsupported swap pair Please change the amount to swap This card might be a production sample or counterfeit Authenticity check failed 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/components/token/TokenItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt index d76f2160fb..8b411abf19 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/TokenItem.kt @@ -29,10 +29,7 @@ import com.tangem.core.ui.components.icons.IconTint import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.token.internal.* import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState -import com.tangem.core.ui.components.token.state.TokenItemState.Subtitle2State -import com.tangem.core.ui.components.token.state.TokenItemState.PromoBannerState -import com.tangem.core.ui.components.token.state.TokenItemState.TitleState +import com.tangem.core.ui.components.token.state.TokenItemState.* import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.rememberHapticFeedback import com.tangem.core.ui.extensions.stringReference @@ -519,15 +516,14 @@ private fun calculateLayoutHeight( is TokenItemState.Content, is TokenItemState.Loading, is TokenItemState.Locked, - -> { - firstColumnHeight = 2 * layoutPadding + title.height + (cryptoAmount?.height ?: 0) - secondColumnHeight = 2 * layoutPadding + (fiatAmount?.height ?: 0) + (priceChange?.height ?: 0) - } - is TokenItemState.Draggable, is TokenItemState.NoAddress, is TokenItemState.Unreachable, -> { - firstColumnHeight = minLayoutHeight + firstColumnHeight = 2 * layoutPadding + title.height + (priceChange?.height ?: 0) + secondColumnHeight = 2 * layoutPadding + (fiatAmount?.height ?: 0) + (cryptoAmount?.height ?: 0) + } + is TokenItemState.Draggable -> { + firstColumnHeight = 2 * layoutPadding + title.height + (cryptoAmount?.height ?: 0) secondColumnHeight = minLayoutHeight } } @@ -536,6 +532,7 @@ private fun calculateLayoutHeight( } @Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, fontScale = 2f) @Composable private fun Preview_TokenItem_InLight(@PreviewParameter(TokenItemStateProvider::class) state: TokenItemState) { TangemThemePreview(isDark = false) { @@ -679,6 +676,7 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider Unit)? = null, ) { val iconColor = getIconColor(type = type, color = color) Row( @@ -86,7 +90,8 @@ fun TangemBadge( .heightIn(min = size.toHeightDp()) .clip(shape.toShape(size)) .getBackgroundColor(type = type, color = color, shape = shape.toShape(size)) - .padding(size.toPaddingDp(position = iconPosition)), + .padding(size.toPaddingDp(position = iconPosition)) + .clickableSingle(enabled = onClick != null, onClick = { onClick?.invoke() }), ) { AnimatedVisibility( visible = iconRes != null && iconPosition == TangemBadgeIconPosition.Start, diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt index e28cbdd6ba..5f666898bb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt @@ -8,13 +8,13 @@ import com.tangem.core.ui.extensions.TextReference * UI model for [TangemBadge] component * * @param text TextReference for the badge label. - * @param modifier Modifier to be applied to the badge. * @param iconRes Drawable resource ID for the icon to be displayed in the badge. * @param size [TangemBadgeSize] defining the size of the badge. * @param shape [TangemBadgeShape] defining the shape of the badge. * @param color [TangemBadgeColor] defining the color scheme of the badge. * @param type [TangemBadgeType] defining the style of the badge. * @param iconPosition [TangemBadgeIconPosition] defining icon position of the badge. + * @param onClick Lambda to be invoked when the badge is clicked (optional). */ class TangemBadgeUM( val text: TextReference, @@ -24,4 +24,5 @@ class TangemBadgeUM( val color: TangemBadgeColor = TangemBadgeColor.Gray, val type: TangemBadgeType = TangemBadgeType.Solid, val iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start, + val onClick: (() -> Unit)? = null, ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/image/TangemIconUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/image/TangemIconUM.kt new file mode 100644 index 0000000000..ff7be95be2 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/image/TangemIconUM.kt @@ -0,0 +1,76 @@ +package com.tangem.core.ui.ds.image + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.Image +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.icons.identicon.IdentIcon +import com.tangem.core.ui.extensions.ColorReference2 +import com.tangem.core.ui.res.TangemTheme + +/** + * Model representing different types of icons that can be displayed in the UI. + */ +@Immutable +sealed interface TangemIconUM { + + /** Icon representing a currency. */ + data class Currency( + val currencyIconState: CurrencyIconState, + ) : TangemIconUM + + /** Icon represented by a drawable resource. */ + data class Icon( + @DrawableRes val iconRes: Int, + val tintReference: ColorReference2 = ColorReference2 { TangemTheme.colors2.graphic.neutral.primary }, + ) : TangemIconUM + + /** Image represented by a drawable resource. */ + data class Image( + @DrawableRes val imageRes: Int, + ) : TangemIconUM + + /** Identicon represented by a text string (e.g., an address). */ + data class Ident( + val text: String, + ) : TangemIconUM +} + +/** + * Composable function to display an icon based on the provided [TangemIconUM] type. + * + * @param tangemIconUM The [TangemIconUM] instance representing the icon to be displayed. + * @param modifier The [Modifier] to be applied to the icon. + */ +@Composable +fun TangemIcon(tangemIconUM: TangemIconUM, modifier: Modifier = Modifier) { + when (tangemIconUM) { + is TangemIconUM.Currency -> { + CurrencyIcon( + state = tangemIconUM.currencyIconState, + modifier = modifier, + ) + } + is TangemIconUM.Icon -> Icon( + imageVector = ImageVector.vectorResource(tangemIconUM.iconRes), + contentDescription = null, + modifier = modifier, + tint = tangemIconUM.tintReference(), + ) + is TangemIconUM.Image -> Image( + imageVector = ImageVector.vectorResource(tangemIconUM.imageRes), + contentDescription = null, + modifier = modifier, + ) + is TangemIconUM.Ident -> IdentIcon( + address = tangemIconUM.text, + modifier = modifier, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt index ff63624792..3591920330 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt @@ -11,9 +11,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -24,6 +26,7 @@ import com.tangem.core.ui.components.flicker import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.ds.button.* +import com.tangem.core.ui.ds.image.TangemIcon import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @@ -35,21 +38,31 @@ import kotlinx.collections.immutable.persistentListOf * * @param messageUM Data model containing message properties. * @param modifier Modifier to be applied to the message component. - * @param content Optional composable content to be displayed alongside the title and subtitle. */ @Composable fun TangemMessage( messageUM: TangemMessageUM, modifier: Modifier = Modifier, - content: @Composable (RowScope.() -> Unit)? = null, + contentColor: Color = TangemTheme.colors2.surface.level3, ) { TangemMessage( - modifier = modifier, + modifier = modifier + .conditional(messageUM.onClick != null) { + clickableSingle(onClick = requireNotNull(messageUM.onClick)) + }, title = messageUM.title, subtitle = messageUM.subtitle, messageEffect = messageUM.messageEffect, isCentered = messageUM.isCentered, - content = content, + content = { + if (messageUM.iconUM != null) { + TangemIcon( + tangemIconUM = messageUM.iconUM, + modifier = Modifier.size(TangemTheme.dimens2.x8), + ) + } + }, + contentColor = contentColor, onCloseClick = messageUM.onCloseClick, buttons = { messageUM.buttonsUM.fastForEach { buttonUM -> @@ -73,7 +86,11 @@ fun TangemMessage( * @see com.tangem.core.ui.components.notifications.Notification for legacy component. */ @Composable -fun TangemMessage(config: NotificationConfig, modifier: Modifier = Modifier) { +fun TangemMessage( + config: NotificationConfig, + modifier: Modifier = Modifier, + contentColor: Color = TangemTheme.colors2.surface.level3, +) { val buttonState = config.buttonsState TangemMessage( title = config.title, @@ -101,6 +118,7 @@ fun TangemMessage(config: NotificationConfig, modifier: Modifier = Modifier) { ) } }, + contentColor = contentColor, buttons = if (buttonState != null) { { TangemMessageLegacyButtons(buttonState = buttonState) @@ -129,10 +147,11 @@ fun TangemMessage( title: TextReference? = null, subtitle: TextReference? = null, messageEffect: TangemMessageEffect = TangemMessageEffect.None, - content: (@Composable RowScope.() -> Unit)? = null, - buttons: (@Composable RowScope.() -> Unit)? = null, onCloseClick: (() -> Unit)? = null, isCentered: Boolean = false, + contentColor: Color = TangemTheme.colors2.surface.level3, + content: (@Composable RowScope.() -> Unit)? = null, + buttons: (@Composable RowScope.() -> Unit)? = null, ) { val alignment = if (isCentered) { Alignment.CenterHorizontally @@ -146,6 +165,7 @@ fun TangemMessage( .messageEffectBackground( messageEffect = messageEffect, radius = TangemTheme.dimens2.x6, + contentColor = contentColor, ), ) Column( @@ -160,8 +180,9 @@ fun TangemMessage( subtitle = subtitle, alignment = alignment, content = content, + isCentered = isCentered, ) - if (buttons != null && !isCentered) { + if (buttons != null) { Row( horizontalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier @@ -191,8 +212,14 @@ private fun TangemMessageContent( title: TextReference? = null, subtitle: TextReference? = null, alignment: Alignment.Horizontal = Alignment.Start, + isCentered: Boolean = false, content: (@Composable RowScope.() -> Unit)? = null, ) { + val textAlign = if (isCentered) { + TextAlign.Center + } else { + TextAlign.Start + } Row( horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), modifier = Modifier.padding(TangemTheme.dimens2.x1), @@ -208,10 +235,12 @@ private fun TangemMessageContent( style = TangemTheme.typography2.bodySemibold16, color = TangemTheme.colors2.text.neutral.primary, maxLines = 1, + textAlign = textAlign, ) } if (subtitle != null) { Text( + textAlign = textAlign, text = subtitle.resolveAnnotatedReference(), style = TangemTheme.typography2.captionSemibold12, color = TangemTheme.colors2.text.neutral.secondary, @@ -310,12 +339,14 @@ private class TangemMessagePreviewProvider : PreviewParameterProvider get() = sequenceOf( TangemMessageUM( + id = "1", title = stringReference("Title text"), subtitle = stringReference("Subtext"), messageEffect = TangemMessageEffect.None, isCentered = true, ), TangemMessageUM( + id = "2", title = stringReference("Title text"), subtitle = stringReference("Subtext"), messageEffect = TangemMessageEffect.Magic, @@ -335,6 +366,7 @@ private class TangemMessagePreviewProvider : PreviewParameterProvider = persistentListOf(), + val onClick: (() -> Unit)? = null, val onCloseClick: (() -> Unit)? = null, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowUM.kt new file mode 100644 index 0000000000..d605343ac6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowUM.kt @@ -0,0 +1,12 @@ +package com.tangem.core.ui.ds.row + +import androidx.compose.runtime.Immutable + +/** + * Base interface for all row UI models in the Tangem application. Each row UI model must implement this interface + */ +@Immutable +interface TangemRowUM { + + val id: String +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt new file mode 100644 index 0000000000..f5af27f6c1 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt @@ -0,0 +1,230 @@ +package com.tangem.core.ui.ds.row.header + +import android.content.res.Configuration +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.ds.image.TangemIcon +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.clickableSingle +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.test.TokenElementsTestTags + +/** + * UI model for header row component + * + * @param headerRowUM UI model for the header row + * @param modifier Modifier for the composable + */ +@Composable +fun TangemHeaderRow(headerRowUM: TangemHeaderRowUM, modifier: Modifier = Modifier) { + TangemHeaderRow( + headTangemIconUM = headerRowUM.startIconUM, + footerTangemIconRes = headerRowUM.endIconRes, + title = headerRowUM.title, + subtitle = headerRowUM.subtitle, + modifier = modifier, + ) +} + +/** + * Composable function that represents a header row with customizable title and head content. + * + * @param modifier Modifier for the composable + * @param subtitle Optional subtitle as a TextReference + * @param onItemClick Optional click callback for the row + * @param footerTangemIconRes Optional drawable resource ID for the footer icon + * @param titleContent Composable lambda for the title content + * @param headContent Composable lambda for the head content + */ +@Composable +fun TangemHeaderRow( + modifier: Modifier = Modifier, + subtitle: TextReference? = null, + onItemClick: (() -> Unit)? = null, + @DrawableRes footerTangemIconRes: Int? = null, + titleContent: @Composable (Modifier) -> Unit, + headContent: @Composable (Modifier) -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .clickableSingle(enabled = onItemClick != null, onClick = { onItemClick?.invoke() }) + .padding( + top = TangemTheme.dimens2.x4, + bottom = TangemTheme.dimens2.x3, + start = TangemTheme.dimens2.x4, + end = TangemTheme.dimens2.x4, + ), + ) { + headContent( + Modifier + .padding(end = TangemTheme.dimens2.x2) + .size(TangemTheme.dimens2.x4), + ) + titleContent(Modifier) + AnimatedVisibility( + visible = subtitle != null, + ) { + val wrappedSubtitle = remember(this) { requireNotNull(subtitle) } + Text( + text = wrappedSubtitle.resolveAnnotatedReference(), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + modifier = Modifier + .testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT) + .padding(start = TangemTheme.dimens2.x1), + ) + } + SpacerWMax() + AnimatedVisibility( + visible = footerTangemIconRes != null, + ) { + val wrappedIconUM = remember(this) { requireNotNull(footerTangemIconRes) } + Icon( + imageVector = ImageVector.vectorResource(id = wrappedIconUM), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.secondary, + modifier = Modifier.size(TangemTheme.dimens2.x4), + ) + } + } +} + +/** + * Composable function that represents a header row with title, optional subtitle, and optional icons. + * + * @param title Title as a TextReference + * @param modifier Modifier for the composable + * @param subtitle Optional subtitle as a TextReference + * @param headTangemIconUM Optional TangemIconUM for the head icon + * @param footerTangemIconRes Optional drawable resource ID for the footer icon + * @param isEnabled Boolean indicating if the row is clickable + * @param onItemClick Optional click callback for the row + */ +@Composable +fun TangemHeaderRow( + title: TextReference, + modifier: Modifier = Modifier, + subtitle: TextReference? = null, + headTangemIconUM: TangemIconUM? = null, + @DrawableRes footerTangemIconRes: Int? = null, + isEnabled: Boolean = false, + onItemClick: (() -> Unit)? = null, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .clickableSingle(enabled = isEnabled && onItemClick != null, onClick = { onItemClick?.invoke() }) + .padding( + top = TangemTheme.dimens2.x4, + bottom = TangemTheme.dimens2.x3, + start = TangemTheme.dimens2.x4, + end = TangemTheme.dimens2.x4, + ), + ) { + AnimatedVisibility( + visible = headTangemIconUM != null, + ) { + val wrappedIconUM = remember(this) { requireNotNull(headTangemIconUM) } + TangemIcon( + tangemIconUM = wrappedIconUM, + modifier = Modifier + .padding(end = TangemTheme.dimens2.x2) + .size(TangemTheme.dimens2.x4), + ) + } + Text( + text = title.resolveAnnotatedReference(), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + modifier = Modifier.testTag(tag = TokenElementsTestTags.TOKEN_TITLE), + ) + AnimatedVisibility( + visible = subtitle != null, + ) { + val wrappedSubtitle = remember(this) { requireNotNull(subtitle) } + Text( + text = wrappedSubtitle.resolveAnnotatedReference(), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + modifier = Modifier + .testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT) + .padding(start = TangemTheme.dimens2.x1), + ) + } + SpacerWMax() + AnimatedVisibility( + visible = footerTangemIconRes != null, + ) { + val wrappedIconUM = remember(this) { requireNotNull(footerTangemIconRes) } + Icon( + imageVector = ImageVector.vectorResource(id = wrappedIconUM), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.secondary, + modifier = Modifier.size(TangemTheme.dimens2.x4), + ) + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TangemHeaderRow_Preview(@PreviewParameter(PreviewProvider::class) params: TangemHeaderRowUM) { + TangemThemePreviewRedesign { + TangemHeaderRow( + headerRowUM = params, + modifier = Modifier.background(TangemTheme.colors2.surface.level3), + ) + } +} + +private class PreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + TangemHeaderRowUM( + id = "1", + startIconUM = TangemIconUM.Currency( + currencyIconState = CurrencyIconState.Locked, + ), + endIconRes = R.drawable.ic_minimize_24, + title = stringReference("Account"), + subtitle = stringReference("\$ 42,900.17"), + ), + TangemHeaderRowUM( + id = "2", + title = stringReference("Account"), + ), + ) +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRowUM.kt new file mode 100644 index 0000000000..fd69abb660 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRowUM.kt @@ -0,0 +1,29 @@ +package com.tangem.core.ui.ds.row.header + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.row.TangemRowUM +import com.tangem.core.ui.extensions.TextReference + +/** + * UI model for header row component + * + * @param id Unique id + * @param title Title text reference + * @param subtitle Subtitle text reference (optional) + * @param startIconUM Icon UI model (optional) + * @param endIconRes Icon UI model (optional) + * @param isEnabled Flag indicating if click is enabled + * @param onItemClick Callback for item click (optional) + */ +@Immutable +data class TangemHeaderRowUM( + override val id: String, + val title: TextReference, + val subtitle: TextReference? = null, + val startIconUM: TangemIconUM? = null, + @DrawableRes val endIconRes: Int? = null, + val isEnabled: Boolean = false, + val onItemClick: (() -> Unit)? = null, +) : TangemRowUM \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt index 8f9c251ea1..a0b50fe199 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.ds.image.TangemIcon import com.tangem.core.ui.ds.row.TangemRowContainer import com.tangem.core.ui.ds.row.TangemRowLayoutId import com.tangem.core.ui.ds.row.token.internal.* @@ -44,19 +44,19 @@ fun TangemTokenRow( ) { TangemRowContainer( content = { - CurrencyIcon( - state = tokenRowUM.iconState, + TangemIcon( + tangemIconUM = tokenRowUM.headIconUM, modifier = Modifier .layoutId(layoutId = TangemRowLayoutId.HEAD) .padding(end = TangemTheme.dimens2.x2) - .testTag(TokenElementsTestTags.TOKEN_ICON), + .testTag(tag = TokenElementsTestTags.TOKEN_ICON), ) TokenRowPromoBanner( promoBannerUM = tokenRowUM.promoBannerUM, modifier = Modifier .layoutId(layoutId = TangemRowLayoutId.EXTRA_TOP) - .testTag(TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER) + .testTag(tag = TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER) .padding(horizontal = TangemTheme.dimens2.x3) .fillMaxWidth(), ) @@ -98,7 +98,89 @@ fun TangemTokenRow( reorderableTokenListState = reorderableTokenListState, modifier = Modifier .layoutId(layoutId = TangemRowLayoutId.TAIL) - .testTag(TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK), + .testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK), + ) + }, + modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM), + ) +} + +/** + * Composable function that represents a Tangem token row in a list. + * + * [Token Row](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8207-17583&t=k8dyaykorsNocGVq-4) + * + * @param tokenRowUM The user model containing the data for the token row. + * @param headComponent The composable function representing the head component. + * @param titleComponent The composable function representing the title component. + * @param isBalanceHidden A boolean indicating whether the balance should be hidden. + * @param reorderableTokenListState The state of the reorderable lazy list, if applicable. + * @param modifier The modifier to be applied to the row. + */ +@Composable +fun TangemTokenRow( + tokenRowUM: TangemTokenRowUM, + isBalanceHidden: Boolean, + reorderableTokenListState: ReorderableLazyListState?, + modifier: Modifier = Modifier, + headComponent: @Composable (Modifier) -> Unit, + titleComponent: @Composable (Modifier) -> Unit, +) { + TangemRowContainer( + content = { + headComponent( + Modifier + .layoutId(layoutId = TangemRowLayoutId.HEAD) + .padding(end = TangemTheme.dimens2.x2) + .testTag(tag = TokenElementsTestTags.TOKEN_ICON), + ) + + TokenRowPromoBanner( + promoBannerUM = tokenRowUM.promoBannerUM, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.EXTRA_TOP) + .testTag(tag = TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER) + .padding(horizontal = TangemTheme.dimens2.x3) + .fillMaxWidth(), + ) + + titleComponent( + Modifier + .layoutId(layoutId = TangemRowLayoutId.START_TOP) + .padding(end = TangemTheme.dimens2.x2) + .testTag(tag = TokenElementsTestTags.TOKEN_TITLE), + ) + + TokenRowSubtitle( + subtitleUM = tokenRowUM.subtitleUM, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.START_BOTTOM) + .padding(end = TangemTheme.dimens2.x2) + .testTag(tag = TokenElementsTestTags.TOKEN_PRICE), + ) + + TokenRowEndTopContent( + endContentUM = tokenRowUM.topEndContentUM, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.END_TOP) + .testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT), + ) + + TokenRowEndBottomContent( + endContentUM = tokenRowUM.bottomEndContentUM, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.END_BOTTOM) + .testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT), + ) + + TokenRowTail( + tailUM = tokenRowUM.tailUM, + reorderableTokenListState = reorderableTokenListState, + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.TAIL) + .testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK), ) }, modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM), @@ -114,7 +196,7 @@ private fun Modifier.tokenClickable(tokenRowUM: TangemTokenRowUM): Modifier = co val onHapticLongClick = if (onLongClick != null) { { hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - onLongClick(tokenRowUM) + onLongClick() } } else { null @@ -123,9 +205,9 @@ private fun Modifier.tokenClickable(tokenRowUM: TangemTokenRowUM): Modifier = co when { onClick == null && onLongClick == null -> this onClick == null && onLongClick != null -> combinedClickable(onClick = {}, onLongClick = onHapticLongClick) - onClick != null && onLongClick == null -> combinedClickable(onClick = { onClick(tokenRowUM) }) + onClick != null && onLongClick == null -> combinedClickable(onClick = onClick) onClick != null && onLongClick != null -> { - combinedClickable(onClick = { onClick(tokenRowUM) }, onLongClick = onHapticLongClick) + combinedClickable(onClick = onClick, onLongClick = onHapticLongClick) } else -> this } diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt index 8ef54a567b..98027ff5dd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRowUM.kt @@ -4,20 +4,20 @@ import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.ds.badge.TangemBadgeUM -import com.tangem.core.ui.extensions.ColorReference2 +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.row.TangemRowUM import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.res.TangemTheme import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @Immutable -sealed class TangemTokenRowUM { +sealed class TangemTokenRowUM : TangemRowUM { /** Unique id */ - abstract val id: String + abstract override val id: String /** Token icon state */ - abstract val iconState: CurrencyIconState + abstract val headIconUM: TangemIconUM.Currency /** Token title UM (in one row with [topEndContentUM]) */ abstract val titleUM: TitleUM @@ -38,25 +38,25 @@ sealed class TangemTokenRowUM { abstract val promoBannerUM: PromoBannerUM /** Callback which will be called when an item is clicked */ - abstract val onItemClick: ((TangemTokenRowUM) -> Unit)? + abstract val onItemClick: (() -> Unit)? /** Callback which will be called when an item is long clicked */ - abstract val onItemLongClick: ((TangemTokenRowUM) -> Unit)? + abstract val onItemLongClick: (() -> Unit)? /** * Content state of [TangemTokenRowUM] */ data class Content( override val id: String, - override val iconState: CurrencyIconState, + override val headIconUM: TangemIconUM.Currency, override val titleUM: TitleUM, override val subtitleUM: SubtitleUM, override val topEndContentUM: EndContentUM, override val bottomEndContentUM: EndContentUM, override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty, override val tailUM: TailUM = TailUM.Empty, - override val onItemClick: ((TangemTokenRowUM) -> Unit)?, - override val onItemLongClick: ((TangemTokenRowUM) -> Unit)?, + override val onItemClick: (() -> Unit)?, + override val onItemLongClick: (() -> Unit)?, ) : TangemTokenRowUM() /** @@ -64,7 +64,7 @@ sealed class TangemTokenRowUM { */ data class Loading( override val id: String, - override val iconState: CurrencyIconState, + override val headIconUM: TangemIconUM.Currency = TangemIconUM.Currency(CurrencyIconState.Loading), override val titleUM: TitleUM = TitleUM.Loading, override val subtitleUM: SubtitleUM = SubtitleUM.Loading, ) : TangemTokenRowUM() { @@ -72,8 +72,8 @@ sealed class TangemTokenRowUM { override val bottomEndContentUM: EndContentUM = EndContentUM.Loading override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty override val tailUM: TailUM = TailUM.Empty - override val onItemClick: ((TangemTokenRowUM) -> Unit)? = null - override val onItemLongClick: ((TangemTokenRowUM) -> Unit)? = null + override val onItemClick: (() -> Unit)? = null + override val onItemLongClick: (() -> Unit)? = null } /** @@ -81,12 +81,12 @@ sealed class TangemTokenRowUM { */ data class Actionable( override val id: String, - override val iconState: CurrencyIconState, + override val headIconUM: TangemIconUM.Currency, override val titleUM: TitleUM, override val subtitleUM: SubtitleUM, override val tailUM: TailUM, - override val onItemClick: ((TangemTokenRowUM) -> Unit)?, - override val onItemLongClick: ((TangemTokenRowUM) -> Unit)?, + override val onItemClick: (() -> Unit)?, + override val onItemLongClick: (() -> Unit)?, override val topEndContentUM: EndContentUM = EndContentUM.Empty, override val bottomEndContentUM: EndContentUM = EndContentUM.Empty, ) : TangemTokenRowUM() { @@ -116,7 +116,7 @@ sealed class TangemTokenRowUM { val text: TextReference, val isAvailable: Boolean = true, val isFlickering: Boolean = false, - val icons: ImmutableList = persistentListOf(), + val icons: ImmutableList = persistentListOf(), val priceChangeUM: PriceChangeState = PriceChangeState.Unknown, val badge: TangemBadgeUM? = null, ) : SubtitleUM() @@ -133,7 +133,7 @@ sealed class TangemTokenRowUM { val text: TextReference, val isAvailable: Boolean = true, val isFlickering: Boolean = false, - val icons: ImmutableList = persistentListOf(), + val icons: ImmutableList = persistentListOf(), val priceChangeUM: PriceChangeState = PriceChangeState.Unknown, ) : EndContentUM() @@ -164,9 +164,4 @@ sealed class TangemTokenRowUM { data object Empty : TailUM() } - - data class IconUM( - val iconRes: Int, - val tintReference: ColorReference2 = ColorReference2 { TangemTheme.colors2.graphic.neutral.primary }, - ) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt index aee2125611..ce68a8771d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.stringReference @@ -63,49 +64,60 @@ internal object TangemTokenRowPreviewData { text = stringReference("Title"), ) - private val accountResIcon: CurrencyIconState.CryptoPortfolio.Icon - get() = CurrencyIconState.CryptoPortfolio.Icon( - resId = R.drawable.ic_rounded_star_24, - color = Color.Blue, - isGrayscale = false, - ) - private val accountLetterIcon: CurrencyIconState.CryptoPortfolio.Letter - get() = CurrencyIconState.CryptoPortfolio.Letter( - char = stringReference("A"), - color = Color.Blue, - isGrayscale = false, + private val accountResIcon: TangemIconUM.Currency + get() = TangemIconUM.Currency( + CurrencyIconState.CryptoPortfolio.Icon( + resId = R.drawable.ic_rounded_star_24, + color = Color.Blue, + isGrayscale = false, + ), ) - private val coinIconState - get() = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = R.drawable.img_polygon_22, - isGrayscale = false, - shouldShowCustomBadge = false, + private val accountLetterIcon: TangemIconUM.Currency + get() = TangemIconUM.Currency( + CurrencyIconState.CryptoPortfolio.Letter( + char = stringReference("A"), + color = Color.Blue, + isGrayscale = false, + ), ) - private val tokenIconState - get() = CurrencyIconState.TokenIcon( - url = null, - topBadgeIconResId = R.drawable.img_polygon_22, - fallbackTint = TangemColorPalette.Black, - fallbackBackground = TangemColorPalette.Meadow, - isGrayscale = false, - shouldShowCustomBadge = false, + private val coinIconState: TangemIconUM.Currency + get() = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_polygon_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), ) - private val customTokenIconState - get() = CurrencyIconState.CustomTokenIcon( - tint = TangemColorPalette.Black, - background = TangemColorPalette.Meadow, - topBadgeIconResId = R.drawable.img_polygon_22, - isGrayscale = false, + private val tokenIconState: TangemIconUM.Currency + get() = TangemIconUM.Currency( + CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_polygon_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ) + + private val customTokenIconState: TangemIconUM.Currency + get() = TangemIconUM.Currency( + CurrencyIconState.CustomTokenIcon( + tint = TangemColorPalette.Black, + background = TangemColorPalette.Meadow, + topBadgeIconResId = R.drawable.img_polygon_22, + isGrayscale = false, + ), ) val defaultState: TangemTokenRowUM.Content get() = TangemTokenRowUM.Content( id = UUID.randomUUID().toString(), - iconState = coinIconState, + headIconUM = coinIconState, titleUM = titleUM, subtitleUM = subtitleUM, topEndContentUM = topEndContentUM, @@ -131,9 +143,9 @@ internal object TangemTokenRowPreviewData { }), ), icons = persistentListOf( - TangemTokenRowUM.IconUM(R.drawable.ic_staking_mini_10), - TangemTokenRowUM.IconUM(R.drawable.ic_attention_12), - TangemTokenRowUM.IconUM(R.drawable.ic_error_sync_24), + TangemIconUM.Icon(R.drawable.ic_staking_mini_10), + TangemIconUM.Icon(R.drawable.ic_attention_12), + TangemIconUM.Icon(R.drawable.ic_error_sync_24), ), ), bottomEndContentUM = bottomEndContentUM, @@ -146,7 +158,7 @@ internal object TangemTokenRowPreviewData { val tokenState: TangemTokenRowUM.Content get() = TangemTokenRowUM.Content( id = UUID.randomUUID().toString(), - iconState = tokenIconState, + headIconUM = tokenIconState, titleUM = titleUM, subtitleUM = subtitleUM, topEndContentUM = topEndContentUM, @@ -160,7 +172,7 @@ internal object TangemTokenRowPreviewData { val customTokenState: TangemTokenRowUM.Content get() = TangemTokenRowUM.Content( id = UUID.randomUUID().toString(), - iconState = customTokenIconState, + headIconUM = customTokenIconState, titleUM = titleUM, subtitleUM = subtitleUM, topEndContentUM = topEndContentUM, @@ -174,7 +186,7 @@ internal object TangemTokenRowPreviewData { val draggableState: TangemTokenRowUM.Actionable get() = TangemTokenRowUM.Actionable( id = UUID.randomUUID().toString(), - iconState = coinIconState, + headIconUM = coinIconState, titleUM = titleUM, subtitleUM = subtitleUM, tailUM = TangemTokenRowUM.TailUM.Draggable, @@ -185,7 +197,7 @@ internal object TangemTokenRowPreviewData { val draggableStateV2: TangemTokenRowUM.Actionable get() = TangemTokenRowUM.Actionable( id = UUID.randomUUID().toString(), - iconState = coinIconState, + headIconUM = coinIconState, titleUM = titleUM, subtitleUM = subtitleUM, topEndContentUM = topEndContentUM, @@ -198,12 +210,12 @@ internal object TangemTokenRowPreviewData { val loadingState: TangemTokenRowUM.Loading get() = TangemTokenRowUM.Loading( id = UUID.randomUUID().toString(), - iconState = coinIconState, + headIconUM = coinIconState, titleUM = TangemTokenRowUM.TitleUM.Loading, subtitleUM = TangemTokenRowUM.SubtitleUM.Loading, ) - val unreachableState: TangemTokenRowUM.Content + val unreachableState: TangemTokenRowUM get() = defaultState.copy( topEndContentUM = TangemTokenRowUM.EndContentUM.Content( text = stringReference(StringsSigns.DASH_SIGN), @@ -219,7 +231,7 @@ internal object TangemTokenRowPreviewData { val accountState: TangemTokenRowUM.Content get() = TangemTokenRowUM.Content( id = UUID.randomUUID().toString(), - iconState = accountResIcon, + headIconUM = accountResIcon, titleUM = TangemTokenRowUM.TitleUM.Content( text = stringReference(value = "Portfolio"), ), @@ -239,7 +251,7 @@ internal object TangemTokenRowPreviewData { val accountLetterState: TangemTokenRowUM.Content get() = accountState.copy( - iconState = accountLetterIcon, + headIconUM = accountLetterIcon, ) val accountEllipsisState: TangemTokenRowUM.Content diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt index a4bdecca54..376e670bda 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt @@ -65,9 +65,9 @@ private fun Content( ), ) - when (endContentUM.priceChangeUM) { + when (val priceChangeUM = endContentUM.priceChangeUM) { is PriceChangeState.Content -> TokenRowPriceChangeContent( - priceChangeState = endContentUM.priceChangeUM, + priceChangeState = priceChangeUM, isFlickering = endContentUM.isFlickering, isAvailable = endContentUM.isAvailable, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowSubtitle.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowSubtitle.kt index c0d32baca0..4bcdac0586 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowSubtitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowSubtitle.kt @@ -65,9 +65,9 @@ private fun SubtitleContent(subtitleUM: TangemTokenRowUM.SubtitleUM.Content, mod ), ) - when (subtitleUM.priceChangeUM) { + when (val priceChangeUM = subtitleUM.priceChangeUM) { is PriceChangeState.Content -> TokenRowPriceChangeContent( - priceChangeState = subtitleUM.priceChangeUM, + priceChangeState = priceChangeUM, isFlickering = subtitleUM.isFlickering, isAvailable = subtitleUM.isAvailable, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTitle.kt index ad8368ab4d..65f1334bee 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowTitle.kt @@ -21,12 +21,12 @@ import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.ds.badge.TangemBadge import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.extensions.conditional -import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resolveAnnotatedReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @Composable -internal fun TokenRowTitle(titleUM: TangemTokenRowUM.TitleUM, modifier: Modifier = Modifier) { +fun TokenRowTitle(titleUM: TangemTokenRowUM.TitleUM, modifier: Modifier = Modifier) { when (titleUM) { is TangemTokenRowUM.TitleUM.Content -> ContentTitle(titleUM = titleUM, modifier = modifier) TangemTokenRowUM.TitleUM.Loading -> TextShimmer( @@ -42,7 +42,7 @@ internal fun TokenRowTitle(titleUM: TangemTokenRowUM.TitleUM, modifier: Modifier private fun ContentTitle(titleUM: TangemTokenRowUM.TitleUM.Content, modifier: Modifier = Modifier) { Row( modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens2.x4), + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens2.x1), verticalAlignment = Alignment.CenterVertically, ) { /* @@ -50,7 +50,7 @@ private fun ContentTitle(titleUM: TangemTokenRowUM.TitleUM.Content, modifier: Mo * So we need to use [weight] to avoid displacement. */ Text( - text = titleUM.text.resolveReference(), + text = titleUM.text.resolveAnnotatedReference(), modifier = Modifier.weight(weight = 1f, fill = false), color = if (titleUM.isAvailable) { TangemTheme.colors2.text.neutral.primary diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBarInner.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBarInner.kt index 1014c640e4..e445ea4a30 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBarInner.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBarInner.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalDensity import com.tangem.core.ui.extensions.clickableSingle import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.conditionalCompose @@ -37,16 +38,20 @@ internal fun TangemTopBarInner( onEndContentClick: (() -> Unit)? = null, isGhostButtons: Boolean = false, ) { + val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(density = this).toDp() } Box( modifier = modifier - .height(TangemTheme.dimens2.x16) + .height(TangemTheme.dimens2.x16 + statusBarHeight) .fillMaxWidth() + .padding(top = statusBarHeight) .padding(TangemTheme.dimens2.x4, TangemTheme.dimens2.x3), ) { val iconModifier = Modifier .size(TangemTheme.dimens2.x10) .clip(RoundedCornerShape(TangemTheme.dimens2.x25)) - .background(TangemTheme.colors2.button.backgroundSecondary) + .conditionalCompose(isGhostButtons) { + background(TangemTheme.colors2.button.backgroundSecondary) + } AnimatedVisibility( visible = startContent != null, diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/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/extensions/TextReference.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt index e771427974..1b734bd384 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/TextReference.kt @@ -384,6 +384,13 @@ fun TextReference.orMaskWithStars(maskWithStars: Boolean): TextReference { return if (maskWithStars) stringReference(THREE_STARS) else this } +/** + * Returns the TextReference itself if it's not null, otherwise returns an empty TextReference. + */ +fun TextReference?.orEmpty(): TextReference { + return this ?: TextReference.EMPTY +} + @ReadOnlyComposable @Composable private fun createStyledText( diff --git a/core/ui/src/main/java/com/tangem/core/ui/security/DisableAutofill.kt b/core/ui/src/main/java/com/tangem/core/ui/security/DisableAutofill.kt new file mode 100644 index 0000000000..8693a0ea3e --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/security/DisableAutofill.kt @@ -0,0 +1,31 @@ +package com.tangem.core.ui.security + +import android.os.Build +import android.view.View +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.platform.LocalView + +/** + * Disables autofill for the current Compose view hierarchy. + * + * This prevents password managers and other autofill services from accessing + * sensitive content (e.g., seed phrases, private keys) entered in text fields. + * + * Must be called within a Composable scope before the text fields that need protection. + * + * The original autofill setting is restored when this composable leaves the composition. + */ +@Composable +fun DisableAutofillEffect() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val view = LocalView.current + DisposableEffect(view) { + val previousValue = view.importantForAutofill + view.importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO_EXCLUDE_DESCENDANTS + onDispose { + view.importantForAutofill = previousValue + } + } + } +} \ No newline at end of file diff --git a/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..3ddbefea69 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt @@ -1,14 +1,20 @@ package com.tangem.core.ui.test object SwapTokenScreenTestTags { - const val SWAP_BLOCK_HEADER = "SWAP_TOKEN_SCREEN_SWAP_BLOCK" + const val CONTAINER = "SWAP_TOKEN_SCREEN_CONTAINER" + const val SWAP_BLOCK_HEADER = "SWAP_TOKEN_SCREEN_SWAP_BLOCK_HEADER" const val BALANCE = "SWAP_TOKEN_SCREEN_BALANCE" const val SWAP_TEXT_FIELD = "SWAP_TOKEN_SCREEN_SWAP_TEXT_FIELD" const val RECEIVE_TEXT_FIELD = "SWAP_TOKEN_SCREEN_RECEIVE_TEXT_FIELD" + const val SWAP_CARD = "SWAP_TOKEN_SCREEN_SWAP_CARD" + const val RECEIVE_CARD = "SWAP_TOKEN_SCREEN_RECEIVE_CARD" const val RECEIVE_AMOUNT_SHIMMER = "SWAP_TOKEN_SCREEN_RECEIVE_AMOUNT_SHIMMER" const val PROVIDERS_BLOCK = "SWAP_TOKEN_SCREEN_PROVIDERS_BLOCK" const val SWAP_BUTTON = "SWAP_TOKEN_SCREEN_SWAP_BUTTON" const val TOKEN = "SWAP_TOKEN_SCREEN_TOKEN" const val TOKEN_SYMBOL = "SWAP_TOKEN_SCREEN_TOKEN_SYMBOL" const val TOKEN_ICON = "SWAP_TOKEN_SCREEN_TOKEN_ICON" + const val SELECT_TOKEN_ICON = "SWAP_TOKEN_SCREEN_SELECT_TOKEN_ICON" + const val RECEIVE_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT" + const val SWAP_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_SWAP_FIAT_AMOUNT" } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt index 95ffc567c2..f460a6f129 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/DateTimeFormatters.kt @@ -103,9 +103,9 @@ object DateTimeFormatters { */ val localFullDate: DateTimeFormatter by lazy { val locale = Locale.getDefault() - val datePattern = DateFormat.getBestDateTimePattern(locale, "d MMMM") + val datePattern = icuPatternToJodaPattern(DateFormat.getBestDateTimePattern(locale, "d MMMM")) val timeSkeleton = if (is12HourFormat) "h:mm a" else "HH:mm" - val timePattern = DateFormat.getBestDateTimePattern(locale, timeSkeleton) + val timePattern = icuPatternToJodaPattern(DateFormat.getBestDateTimePattern(locale, timeSkeleton)) val fullPattern = "$datePattern, $timePattern" DateTimeFormatterBuilder() .appendPattern(fullPattern) @@ -128,13 +128,26 @@ object DateTimeFormatters { */ fun getBestFormatterBySkeleton(skeleton: String): DateTimeFormatter { val skeletonWithLocale = skeleton.replaceHourLetters() + val icuPattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeletonWithLocale) + val jodaPattern = icuPatternToJodaPattern(icuPattern) return DateTimeFormatterBuilder() - .appendPattern(DateFormat.getBestDateTimePattern(Locale.getDefault(), skeletonWithLocale)) + .appendPattern(jodaPattern) .toFormatter() .withLocale(Locale.getDefault()) } + /** + * Converts ICU date/time pattern (from [DateFormat.getBestDateTimePattern]) to Joda-Time compatible pattern. + */ + internal fun icuPatternToJodaPattern(icuPattern: String): String { + return icuPattern + .replace("LLLL", "MMMM") + .replace("LLL", "MMM") + .replace("LL", "MM") + .replace("L", "M") + } + private fun String.replaceHourLetters(): String { return if (is12HourFormat) { this.replace('H', 'h').replace('k', 'K') diff --git a/core/ui/src/main/res/drawable-hdpi/img_tangem_wallet_72.webp b/core/ui/src/main/res/drawable-hdpi/img_tangem_wallet_72.webp new file mode 100644 index 0000000000..a82e95d6ad Binary files /dev/null and b/core/ui/src/main/res/drawable-hdpi/img_tangem_wallet_72.webp differ diff --git a/core/ui/src/main/res/drawable-mdpi/img_tangem_wallet_72.webp b/core/ui/src/main/res/drawable-mdpi/img_tangem_wallet_72.webp new file mode 100644 index 0000000000..bdfc895e3c Binary files /dev/null and b/core/ui/src/main/res/drawable-mdpi/img_tangem_wallet_72.webp differ diff --git a/core/ui/src/main/res/drawable-xhdpi/img_tangem_wallet_72.webp b/core/ui/src/main/res/drawable-xhdpi/img_tangem_wallet_72.webp new file mode 100644 index 0000000000..31d50c1c29 Binary files /dev/null and b/core/ui/src/main/res/drawable-xhdpi/img_tangem_wallet_72.webp differ diff --git a/core/ui/src/main/res/drawable-xxhdpi/img_tangem_wallet_72.webp b/core/ui/src/main/res/drawable-xxhdpi/img_tangem_wallet_72.webp new file mode 100644 index 0000000000..0fcc24fc07 Binary files /dev/null and b/core/ui/src/main/res/drawable-xxhdpi/img_tangem_wallet_72.webp differ diff --git a/core/ui/src/main/res/drawable-xxxhdpi/img_tangem_wallet_72.webp b/core/ui/src/main/res/drawable-xxxhdpi/img_tangem_wallet_72.webp new file mode 100644 index 0000000000..16beaaee0d Binary files /dev/null and b/core/ui/src/main/res/drawable-xxxhdpi/img_tangem_wallet_72.webp differ diff --git a/core/ui/src/main/res/drawable/ic_attention_default_24.xml b/core/ui/src/main/res/drawable/ic_attention_default_24.xml new file mode 100644 index 0000000000..6442020ee2 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_attention_default_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_dollar_default_24.xml b/core/ui/src/main/res/drawable/ic_dollar_default_24.xml new file mode 100644 index 0000000000..d9cc704417 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_dollar_default_24.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_error_sync_default_24.xml b/core/ui/src/main/res/drawable/ic_error_sync_default_24.xml new file mode 100644 index 0000000000..f86403750c --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_error_sync_default_24.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_exchange_default_24.xml b/core/ui/src/main/res/drawable/ic_exchange_default_24.xml new file mode 100644 index 0000000000..86260f66f7 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_exchange_default_24.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_filter_default_24.xml b/core/ui/src/main/res/drawable/ic_filter_default_24.xml new file mode 100644 index 0000000000..ec0a6ac12a --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_filter_default_24.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_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_more_default_24.xml b/core/ui/src/main/res/drawable/ic_more_default_24.xml new file mode 100644 index 0000000000..afdee216b3 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_more_default_24.xml @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_plus_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/ic_plus_default_24.xml b/core/ui/src/main/res/drawable/ic_plus_default_24.xml new file mode 100644 index 0000000000..cb630c28ca --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_plus_default_24.xml @@ -0,0 +1,13 @@ + + + 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/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateTimeFormattersTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateTimeFormattersTest.kt new file mode 100644 index 0000000000..7fa8bcd9ce --- /dev/null +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/utils/DateTimeFormattersTest.kt @@ -0,0 +1,143 @@ +package com.tangem.core.ui.utils + +import com.google.common.truth.Truth +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** + * Unit tests for [DateTimeFormatters], in particular for conversion of ICU date/time patterns + * to Joda-Time compatible patterns. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DateTimeFormattersTest { + + @Test + fun `converts LLLL to MMMM - full standalone month pattern that crashes on Chinese locale`() { + // Arrange + val icuPattern = "d LLLL" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("d MMMM") + } + + @Test + fun `converts LLL to MMM - short standalone month`() { + // Arrange + val icuPattern = "dd LLL yyyy" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("dd MMM yyyy") + } + + @Test + fun `converts LL to MM - numeric standalone month`() { + // Arrange + val icuPattern = "yyyy-MM-LL" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("yyyy-MM-MM") + } + + @Test + fun `converts single L to M`() { + // Arrange + val icuPattern = "d/L/yyyy" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("d/M/yyyy") + } + + @Test + fun `leaves pattern without L unchanged`() { + // Arrange + val icuPattern = "dd.MM.yyyy HH:mm" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("dd.MM.yyyy HH:mm") + } + + @Test + fun `leaves pattern with M unchanged`() { + // Arrange + val icuPattern = "d MMMM yyyy" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo("d MMMM yyyy") + } + + @Test + fun `handles mixed ICU pattern as returned for Chinese locale - d MMMM`() { + // Arrange + val icuPatternWithStandaloneMonth = "d LLLL" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPatternWithStandaloneMonth) + + // Assert — Joda-Time can parse and format this without IllegalArgumentException + Truth.assertThat(actual).isEqualTo("d MMMM") + } + + @Test + fun `handles empty string`() { + // Arrange + val icuPattern = "" + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEmpty() + } + + @Test + fun `handles pattern with only literal characters`() { + // Arrange + val icuPattern = " 'at' " + + // Act + val actual = DateTimeFormatters.icuPatternToJodaPattern(icuPattern) + + // Assert + Truth.assertThat(actual).isEqualTo(" 'at' ") + } + + @Test + fun `getBestFormatterBySkeleton with d MMMM skeleton produces formatter that does not throw on format`() { + // Arrange + val formatter = DateTimeFormatters.getBestFormatterBySkeleton("d MMMM") + val date = org.joda.time.DateTime(2025, 2, 13, 12, 0, 0, 0) + + // Act & Assert + val formatted = formatter.print(date) + Truth.assertThat(formatted).isNotEmpty() + } + + @Test + fun `getBestFormatterBySkeleton with dd MMMM skeleton produces formatter that does not throw on format`() { + // Arrange + val formatter = DateTimeFormatters.getBestFormatterBySkeleton("dd MMMM") + val date = org.joda.time.DateTime(2025, 2, 13, 12, 0, 0, 0) + + // Act & Assert + val formatted = formatter.print(date) + Truth.assertThat(formatted).isNotEmpty() + } +} \ No newline at end of file diff --git a/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..44a347b369 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountConverterFactoryContainer.kt @@ -1,6 +1,7 @@ package com.tangem.data.account.converter -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.models.wallet.UserWalletId import javax.inject.Inject @@ -10,7 +11,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 +21,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/converter/CryptoPortfolioConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt index daf35d8a49..fdab8240c3 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/CryptoPortfolioConverter.kt @@ -37,9 +37,9 @@ internal class CryptoPortfolioConverter @AssistedInject constructor( tokens = tokens, userWallet = userWallet, accountIndex = value.derivationIndex.toDerivationIndex(), - ).toSet() + ) } else { - emptySet() + emptyList() }, ) } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/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..795c6dbc37 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt @@ -4,7 +4,8 @@ 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.common.wallets.getSyncStrict import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency @@ -20,7 +21,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 +30,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 +41,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..ac2bb1b720 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt @@ -2,9 +2,10 @@ 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.common.wallets.loadAndGet import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.wallet.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -19,7 +20,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 +30,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 +39,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..16fa1c9e44 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducer.kt @@ -4,7 +4,8 @@ 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.common.wallets.getSyncStrict import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency @@ -20,7 +21,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 +32,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 +41,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/DefaultSingleAccountProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountProducer.kt index 3471f7c173..b9bacb8d41 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountProducer.kt @@ -33,10 +33,10 @@ internal class DefaultSingleAccountProducer @AssistedInject constructor( private val dispatchers: CoroutineDispatcherProvider, ) : SingleAccountProducer { - override val fallback: Option + override val fallback: Option get() = none() - override fun produce(): Flow { + override fun produce(): Flow { return singleAccountListSupplier( params = SingleAccountListProducer.Params(userWalletId = params.accountId.userWalletId), ) diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt index 44aa8713fc..fbc60cc6c6 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt @@ -4,9 +4,10 @@ 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.common.wallets.getSyncStrict import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency @@ -17,6 +18,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 +26,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) @@ -45,17 +47,18 @@ internal class WalletAccountListFlowFactory @Inject constructor( return accountsResponseStoreFactory.create(userWallet.walletId).data .filterNotNull() + .filter { it.accounts.isNotEmpty() } .distinctUntilChanged() - .map(converter::convert) + .map { converter.convert(it) } } private fun createForSingleWallet(userWallet: UserWallet): AccountList { val isSingleWalletWithToken = userWallet.requireColdWallet().cardTypesResolver.isSingleWalletWithToken() val currencies = if (isSingleWalletWithToken) { - cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = userWallet).toSet() + cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = userWallet) } else { - setOf(cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet = userWallet)) + listOf(cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet = userWallet)) } return AccountList.empty(userWalletId = userWallet.walletId, cryptoCurrencies = currencies) diff --git a/data/account/src/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..8712b7eae6 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt @@ -6,8 +6,9 @@ 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.common.wallets.getSyncOrNull import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.DerivationIndex @@ -18,7 +19,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 +27,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..5a3723c4ad 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt @@ -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(relaxUnitFun = true) 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.userWallets } returns userWalletsFlow val accountList = AccountList.empty(userWalletId) every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList) @@ -65,8 +65,9 @@ class DefaultMultiAccountListProducerTest { val expected = listOf(accountList) Truth.assertThat(actual).containsExactly(expected) - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets walletAccountListFlowFactory.create(userWalletId) } } @@ -75,7 +76,7 @@ class DefaultMultiAccountListProducerTest { fun `flow will updated if factoryFlow is updated`() = runTest { // Arrange val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow + every { userWalletsListRepository.userWallets } returns userWalletsFlow val accountList = AccountList.empty(userWalletId) val updatedAccountList = AccountList.empty(userWalletId = userWalletId, sortType = TokensSortType.NONE) @@ -97,10 +98,12 @@ class DefaultMultiAccountListProducerTest { // Assert (second emission) Truth.assertThat(secondEmission).containsExactly(listOf(updatedAccountList)) - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets walletAccountListFlowFactory.create(userWalletId) - userWalletsStore.userWallets + userWalletsListRepository.load() + userWalletsListRepository.userWallets walletAccountListFlowFactory.create(userWalletId) } } @@ -109,7 +112,7 @@ class DefaultMultiAccountListProducerTest { fun `flow is filtered the same response`() = runTest { // Arrange val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow + every { userWalletsListRepository.userWallets } returns userWalletsFlow val accountList = AccountList.empty(userWalletId) val factoryFlow = MutableStateFlow(null) @@ -130,10 +133,12 @@ class DefaultMultiAccountListProducerTest { // Assert (second emission) Truth.assertThat(secondEmission).containsExactly(listOf(accountList)) - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets walletAccountListFlowFactory.create(userWalletId) - userWalletsStore.userWallets + userWalletsListRepository.load() + userWalletsListRepository.userWallets walletAccountListFlowFactory.create(userWalletId) } } @@ -143,7 +148,7 @@ class DefaultMultiAccountListProducerTest { fun `flow returns empty list if factory throws exception`() = runTest { // Arrange val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow + every { userWalletsListRepository.userWallets } returns userWalletsFlow val exception = RuntimeException("Converter error") every { walletAccountListFlowFactory.create(userWalletId) } throws exception @@ -155,8 +160,9 @@ class DefaultMultiAccountListProducerTest { val expected = emptyList() Truth.assertThat(actual).containsExactly(expected) - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets walletAccountListFlowFactory.create(userWalletId) } } @@ -164,8 +170,8 @@ class DefaultMultiAccountListProducerTest { @Test fun `flow is empty if userWalletsFlow returns empty flow`() = runTest { // Arrange - val userWalletsFlow = emptyFlow>() - every { userWalletsStore.userWallets } returns userWalletsFlow + val userWalletsFlow = MutableStateFlow>(emptyList()) + every { userWalletsListRepository.userWallets } returns userWalletsFlow // Act val actual = producer.produce().let(::getEmittedValues) @@ -173,7 +179,10 @@ class DefaultMultiAccountListProducerTest { // Assert Truth.assertThat(actual).isEmpty() // no emissions - coVerify(exactly = 1) { userWalletsStore.userWallets } + coVerify(exactly = 1) { + userWalletsListRepository.load() + userWalletsListRepository.userWallets + } coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) } } @@ -181,7 +190,7 @@ class DefaultMultiAccountListProducerTest { fun `flow is empty if factory returns empty flow`() = runTest { // Arrange val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { walletAccountListFlowFactory.create(userWalletId) } returns emptyFlow() @@ -191,8 +200,9 @@ class DefaultMultiAccountListProducerTest { // Assert Truth.assertThat(actual).isEmpty() // no emissions - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets walletAccountListFlowFactory.create(userWalletId) } } @@ -206,7 +216,7 @@ class DefaultMultiAccountListProducerTest { } val userWalletsFlow = MutableStateFlow(listOf(userWallet, userWallet2)) - every { userWalletsStore.userWallets } returns userWalletsFlow + every { userWalletsListRepository.userWallets } returns userWalletsFlow val accountList = AccountList.empty(userWalletId) every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList) @@ -218,8 +228,9 @@ class DefaultMultiAccountListProducerTest { // Assert Truth.assertThat(actual).isEmpty() // no emissions - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets walletAccountListFlowFactory.create(userWalletId) walletAccountListFlowFactory.create(userWalletId2) } diff --git a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducerTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducerTest.kt index daf026b88f..d1df425b53 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiWalletCryptoCurrenciesProducerTest.kt @@ -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,9 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { // Arrange val userTokensResponseFlow = flowOf(null) - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow // Act @@ -72,7 +74,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual.first()).isEqualTo(expected) verifyOrder { - userWalletsStore.getSyncStrict(params.userWalletId) + userWalletsListRepository.userWallets userTokensResponseStore.get(params.userWalletId) } @@ -113,7 +115,9 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin), ) - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow every { @@ -146,7 +150,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder { - userWalletsStore.getSyncStrict(params.userWalletId) + userWalletsListRepository.userWallets userTokensResponseStore.get(params.userWalletId) responseCryptoCurrenciesFactory.createCurrencies( response = userTokensResponse, @@ -188,7 +192,9 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { val cryptoCurrencies = emptySet() - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow every { @@ -213,7 +219,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder { - userWalletsStore.getSyncStrict(params.userWalletId) + userWalletsListRepository.userWallets userTokensResponseStore.get(params.userWalletId) responseCryptoCurrenciesFactory.createCurrencies( response = userTokensResponse, @@ -257,7 +263,9 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { } .buffer(capacity = 5) - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { userTokensResponseStore.get(params.userWalletId) } returns userTokensResponseFlow every { @@ -279,7 +287,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder { - userWalletsStore.getSyncStrict(params.userWalletId) + userWalletsListRepository.userWallets userTokensResponseStore.get(params.userWalletId) } @@ -304,7 +312,9 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { @Test fun `flow is empty if store returns empty flow`() = runTest { // Arrange - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { userTokensResponseStore.get(params.userWalletId) } returns emptyFlow() // Act @@ -316,7 +326,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual.first()).isEqualTo(expected) verifyOrder { - userWalletsStore.getSyncStrict(params.userWalletId) + userWalletsListRepository.userWallets userTokensResponseStore.get(params.userWalletId) } @@ -329,10 +339,13 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { fun `produce throws exception if UserWallet isn't multi-currency wallet`() = runTest { // Arrange val mockUserWallet = mockk { + every { walletId } returns userWallet.walletId every { isMultiCurrency } returns false } - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow // Act val actual = runCatching { producer.produce() }.exceptionOrNull() @@ -345,7 +358,7 @@ internal class DefaultMultiWalletCryptoCurrenciesProducerTest { Truth.assertThat(actual).isInstanceOf(expected::class.java) Truth.assertThat(actual).hasMessageThat().isEqualTo(expected.message) - verifyOrder { userWalletsStore.getSyncStrict(params.userWalletId) } + verifyOrder { userWalletsListRepository.userWallets } verify(inverse = true) { userTokensResponseStore.get(any()) diff --git a/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt index 47b5fb3992..335516f9fd 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt @@ -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,9 @@ class WalletAccountListFlowFactoryTest { every { this@mockk.isMultiCurrency } returns true } - every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val accountsResponse = createGetWalletAccountsResponse(userWalletId) every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore @@ -88,7 +90,8 @@ class WalletAccountListFlowFactoryTest { val expected = accountList Truth.assertThat(actual).containsExactly(expected) - coVerify(ordering = Ordering.SEQUENCE) { + coVerifySequence { + userWalletsListRepository.userWallets accountsResponseStoreFactory.create(userWalletId) accountsResponseStore.data accountListConverterFactory.create(userWallet) @@ -105,7 +108,9 @@ class WalletAccountListFlowFactoryTest { fun `create for single wallet`() = runTest { val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false) - every { userWalletsStore.getSyncStrict(userWallet.walletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val currency = cryptoCurrencyFactory.ethereum every { cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet) } returns currency @@ -114,14 +119,15 @@ class WalletAccountListFlowFactoryTest { val actual = factory.create(userWallet.walletId).let(::getEmittedValues) // Assert - val expected = AccountList.empty(userWalletId = userWallet.walletId, cryptoCurrencies = setOf(currency)) + val expected = AccountList.empty(userWalletId = userWallet.walletId, cryptoCurrencies = listOf(currency)) Truth.assertThat(actual).containsExactly(expected) - coVerify(ordering = Ordering.SEQUENCE) { + coVerifySequence { cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet) } coVerify(inverse = true) { + userWalletsListRepository.userWallets cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = any()) accountsResponseStoreFactory.create(any()) accountsResponseStore.data @@ -134,9 +140,11 @@ class WalletAccountListFlowFactoryTest { fun `flow is created for single wallet with token`() = runTest { val nodl = MockUserWalletFactory.createSingleWalletWithToken() - every { userWalletsStore.getSyncStrict(nodl.walletId) } returns nodl + val userWalletsFlow = MutableStateFlow(listOf(nodl)) - val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() + every { userWalletsListRepository.userWallets } returns userWalletsFlow + + val currencies = cryptoCurrencyFactory.ethereumAndStellar every { cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = nodl) } returns currencies.toList() @@ -148,7 +156,8 @@ class WalletAccountListFlowFactoryTest { val expected = AccountList.empty(userWalletId = nodl.walletId, cryptoCurrencies = currencies) Truth.assertThat(actual).containsExactly(expected) - coVerify(ordering = Ordering.SEQUENCE) { + coVerifySequence { + userWalletsListRepository.userWallets cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = nodl) } @@ -160,4 +169,42 @@ class WalletAccountListFlowFactoryTest { accountListConverter.convert(any()) } } + + @Test + fun `create for multi wallet with empty accounts does not emit`() = runTest { + // Arrange + val userWallet = mockk { + every { this@mockk.walletId } returns userWalletId + every { this@mockk.isMultiCurrency } returns true + } + + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow + + val accountsResponseWithEmptyAccounts = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + group = null, + sort = null, + totalAccounts = 0, + totalArchivedAccounts = 0, + ), + accounts = emptyList(), + unassignedTokens = emptyList(), + ) + every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore + every { accountsResponseStore.data } returns accountsResponseStoreFlow + accountsResponseStoreFlow.value = accountsResponseWithEmptyAccounts + + // Act + val actual = factory.create(userWalletId).let(::getEmittedValues) + + // Assert + Truth.assertThat(actual).isEmpty() + + coVerify(inverse = true) { + accountListConverterFactory.create(any()) + accountListConverter.convert(any()) + } + } } \ No newline at end of file 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..17a2f87acb 100644 --- a/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt @@ -17,9 +17,9 @@ 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 import com.tangem.domain.models.account.Account.CryptoPortfolio import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName @@ -52,7 +52,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 +66,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..afb50f75c3 100644 --- a/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt @@ -8,12 +8,16 @@ 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 -import io.mockk.* +import io.mockk.clearMocks +import io.mockk.coVerifyOrder +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach @@ -23,14 +27,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 +50,7 @@ class DefaultWalletAccountsResponseFactoryTest { @AfterEach fun tearDownEach() { clearMocks( - userWalletsStore, + userWalletsListRepository, cryptoPortfolioCF, cryptoPortfolioConverter, userTokensResponseFactory, @@ -63,7 +67,9 @@ class DefaultWalletAccountsResponseFactoryTest { tokens = emptyList(), ) - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns null + val userWalletsFlow = MutableStateFlow?>(null) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { userTokensResponseFactory.createDefaultResponse( userWallet = null, @@ -89,7 +95,7 @@ class DefaultWalletAccountsResponseFactoryTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - userWalletsStore.getSyncOrNull(userWalletId) + userWalletsListRepository.userWallets userTokensResponseFactory.createDefaultResponse( userWallet = null, networkFactory = networkFactory, @@ -105,7 +111,9 @@ class DefaultWalletAccountsResponseFactoryTest { every { walletId } returns userWalletId } - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val accounts = AccountList.empty(userWallet.walletId).accounts .filterIsInstance() @@ -145,7 +153,7 @@ class DefaultWalletAccountsResponseFactoryTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - userWalletsStore.getSyncOrNull(userWalletId) + userWalletsListRepository.userWallets cryptoPortfolioConverter.convertListBack(accounts) userTokensResponseFactory.createDefaultResponse( userWallet = userWallet, @@ -165,7 +173,9 @@ class DefaultWalletAccountsResponseFactoryTest { val accounts = AccountList.empty(userWallet.walletId).accounts .filterIsInstance() - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val defaultResponse = UserTokensResponse( group = UserTokensResponse.GroupType.NETWORK, @@ -206,7 +216,9 @@ class DefaultWalletAccountsResponseFactoryTest { val userWallet = mockk(relaxed = true) { every { walletId } returns userWalletId } - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val userTokensResponse = UserTokensResponse( group = UserTokensResponse.GroupType.NETWORK, diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt index 2d7a8855d7..a55c9c0da8 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt @@ -6,9 +6,10 @@ import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.data.common.tokens.getDefaultWalletBlockchains import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency @@ -29,7 +30,7 @@ import com.tangem.domain.models.wallet.isMultiCurrency internal class DefaultCardCryptoCurrencyFactory( private val demoConfig: DemoConfig, private val excludedBlockchains: ExcludedBlockchains, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val accountsFeatureToggles: AccountsFeatureToggles, private val walletAccountsFetcher: WalletAccountsFetcher, private val userTokensResponseStore: UserTokensResponseStore, @@ -42,7 +43,7 @@ internal class DefaultCardCryptoCurrencyFactory( userWalletId: UserWalletId, networks: Set, ): Map> { - val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = userWalletId) // multi-currency wallet if (userWallet !is UserWallet.Cold || userWallet.isMultiCurrency) { @@ -67,7 +68,7 @@ internal class DefaultCardCryptoCurrencyFactory( } override suspend fun createByRawId(userWalletId: UserWalletId, network: Network.RawID): List { - val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = userWalletId) val blockchain = network.toBlockchain() diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt index 11d6ab08c9..cff0ae1d6d 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt @@ -11,8 +11,9 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncOrNull import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -24,7 +25,7 @@ import timber.log.Timber @Suppress("LongParameterList") class UserTokensSaver( private val tangemTechApi: TangemTechApi, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val userTokensResponseStore: UserTokensResponseStore, private val dispatchers: CoroutineDispatcherProvider, private val addressesEnricher: UserTokensResponseAddressesEnricher, @@ -59,7 +60,7 @@ class UserTokensSaver( useEnricher: Boolean = true, onFailSend: () -> Unit = {}, ) = withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) + val userWallet = userWalletsListRepository.getSyncOrNull(id = userWalletId) if (userWallet == null) { Timber.e("UserWallet with id $userWalletId not found. Cannot push tokens.") diff --git a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt index c81b908703..9ce3a6b121 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt @@ -13,8 +13,8 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.repository.WalletsRepository @@ -36,7 +36,7 @@ internal object DataCommonModule { @Singleton fun provideCardCryptoCurrencyFactory( excludedBlockchains: ExcludedBlockchains, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, accountsFeatureToggles: AccountsFeatureToggles, walletAccountsFetcher: WalletAccountsFetcher, userTokensResponseStore: UserTokensResponseStore, @@ -45,7 +45,7 @@ internal object DataCommonModule { return DefaultCardCryptoCurrencyFactory( demoConfig = DemoConfig, excludedBlockchains = excludedBlockchains, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, accountsFeatureToggles = accountsFeatureToggles, walletAccountsFetcher = walletAccountsFetcher, userTokensResponseStore = userTokensResponseStore, @@ -71,7 +71,7 @@ internal object DataCommonModule { @Singleton fun provideUserTokensSaver( tangemTechApi: TangemTechApi, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, userTokensResponseStore: UserTokensResponseStore, dispatchers: CoroutineDispatcherProvider, addressesEnricher: UserTokensResponseAddressesEnricher, @@ -81,7 +81,7 @@ internal object DataCommonModule { ): UserTokensSaver { return UserTokensSaver( tangemTechApi = tangemTechApi, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, userTokensResponseStore = userTokensResponseStore, dispatchers = dispatchers, addressesEnricher = addressesEnricher, @@ -109,13 +109,13 @@ internal object DataCommonModule { @Provides @Singleton fun provideWalletServerBinder( - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, appsFlyerStore: AppsFlyerStore, tangemTechApi: TangemTechApi, dispatchers: CoroutineDispatcherProvider, ): WalletServerBinder { return DefaultWalletServerBinder( - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, appsFlyerStore = appsFlyerStore, tangemTechApi = tangemTechApi, dispatchers = dispatchers, diff --git a/data/common/src/main/kotlin/com/tangem/data/common/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/common/src/main/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinder.kt b/data/common/src/main/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinder.kt index f34c04f916..57272ffb53 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinder.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinder.kt @@ -4,21 +4,22 @@ import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter import com.tangem.datasource.local.appsflyer.AppsFlyerStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncOrNull import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext internal class DefaultWalletServerBinder( - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val appsFlyerStore: AppsFlyerStore, private val tangemTechApi: TangemTechApi, private val dispatchers: CoroutineDispatcherProvider, ) : WalletServerBinder { override suspend fun bind(userWalletId: UserWalletId): ApiResponse? { - val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) ?: return null + val userWallet = userWalletsListRepository.getSyncOrNull(id = userWalletId) ?: return null return bind(userWallet) } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt index 1d2b947799..eb90a1efb0 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt @@ -12,10 +12,10 @@ import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.data.common.network.NetworkFactory import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.configs.GenericCardConfig +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -24,6 +24,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.test.core.ProvideTestModels import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested @@ -36,7 +37,7 @@ import org.junit.jupiter.params.ParameterizedTest @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DefaultCardCryptoCurrencyFactoryTest { - private val userWalletsStore: UserWalletsStore = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk() private val userTokensResponseStore: UserTokensResponseStore = mockk() private val excludedBlockchains = ExcludedBlockchains() private val accountsFeatureToggles = mockk() @@ -45,7 +46,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { private val factory = DefaultCardCryptoCurrencyFactory( demoConfig = DemoConfig, excludedBlockchains = excludedBlockchains, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, userTokensResponseStore = userTokensResponseStore, responseCryptoCurrenciesFactory = ResponseCryptoCurrenciesFactory( networkFactory = NetworkFactory(excludedBlockchains = excludedBlockchains), @@ -63,7 +64,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { @BeforeEach fun init() { - clearMocks(userWalletsStore, userTokensResponseStore, accountsFeatureToggles, walletAccountsFetcher, iconUri) + clearMocks(userWalletsListRepository, userTokensResponseStore, accountsFeatureToggles, walletAccountsFetcher, iconUri) mockkStatic(Uri::class) every { Uri.parse(any()) } returns iconUri @@ -78,11 +79,12 @@ internal class DefaultCardCryptoCurrencyFactoryTest { fun `create currencies in ETH for multi-currency wallet`(model: CreateTestModel.MultiWallet) = runTest { // Arrange val userWallet = createMultiWallet() + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) val userTokensResponse = model.userTokensResponse val network = ethereum.network every { accountsFeatureToggles.isFeatureEnabled } returns false - coEvery { userWalletsStore.getSyncStrict(key = userWallet.walletId) } returns userWallet + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse // Act @@ -94,7 +96,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = userWallet.walletId) + userWalletsListRepository.userWallets userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId) } } @@ -130,8 +132,9 @@ internal class DefaultCardCryptoCurrencyFactoryTest { fun `create currencies for single-currency wallet (ETH)`(model: CreateTestModel.SingleWallet) = runTest { // Arrange val userWallet = createSingleWallet() + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) - coEvery { userWalletsStore.getSyncStrict(key = userWallet.walletId) } returns userWallet + every { userWalletsListRepository.userWallets } returns userWalletsFlow // Act val actual = factory.create(userWalletId = userWallet.walletId, network = model.network) @@ -142,7 +145,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = userWallet.walletId) + userWalletsListRepository.userWallets userWallet.scanResponse.cardTypesResolver.getBlockchain() } @@ -168,8 +171,9 @@ internal class DefaultCardCryptoCurrencyFactoryTest { ) = runTest { // Arrange val userWallet = MockUserWalletFactory.createSingleWalletWithToken() + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) - coEvery { userWalletsStore.getSyncStrict(key = userWallet.walletId) } returns userWallet + every { userWalletsListRepository.userWallets } returns userWalletsFlow // Act val actual = factory.create(userWalletId = userWallet.walletId, network = model.network) @@ -186,7 +190,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { Truth.assertThat(actual).isEqualTo(expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = userWallet.walletId) + userWalletsListRepository.userWallets userWallet.scanResponse.cardTypesResolver.getBlockchain() } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt index 6c79df0d81..16bb9deefe 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt @@ -8,12 +8,13 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -23,7 +24,7 @@ import org.junit.jupiter.api.TestInstance class UserTokensSaverTest { private val tangemTechApi: TangemTechApi = mockk() - private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true) + private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxed = true) private val enricher: UserTokensResponseAddressesEnricher = mockk() private val accountsFeatureToggles = mockk { @@ -34,7 +35,7 @@ class UserTokensSaverTest { private val userTokensSaver: UserTokensSaver = UserTokensSaver( tangemTechApi = tangemTechApi, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, userTokensResponseStore = userTokensResponseStore, dispatchers = TestingCoroutineDispatcherProvider(), addressesEnricher = enricher, @@ -48,7 +49,7 @@ class UserTokensSaverTest { fun resetMocks() { clearMocks( tangemTechApi, - userWalletsStore, + userWalletsListRepository, userTokensResponseStore, enricher, walletServerBinder, @@ -118,8 +119,10 @@ class UserTokensSaverTest { val error = ApiResponseError.UnknownException(Exception("API Error")) var onFailSendCalled = false + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + every { accountsFeatureToggles.isFeatureEnabled } returns true - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { enricher(userWalletId, response) } returns enrichedResponse coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse @@ -132,6 +135,7 @@ class UserTokensSaverTest { // THEN coVerifyOrder { + userWalletsListRepository.userWallets enricher(userWalletId, response) tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse) } @@ -165,7 +169,9 @@ class UserTokensSaverTest { walletType = WalletType.COLD, ) - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { enricher(userWalletId, response) } returns enrichedResponse coEvery { tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse) @@ -177,6 +183,7 @@ class UserTokensSaverTest { // THEN coVerifyOrder { enricher(userWalletId, response) + userWalletsListRepository.userWallets tangemTechApi.saveTokens(userWalletId.stringValue, enrichedResponse) } } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinderTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinderTest.kt index 6da352653b..c1c4e5ceef 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinderTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/wallet/DefaultWalletServerBinderTest.kt @@ -7,13 +7,14 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.WalletIdBody import com.tangem.datasource.api.tangemTech.models.WalletType import com.tangem.datasource.local.appsflyer.AppsFlyerStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.AppsFlyerConversionData import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Nested @@ -26,13 +27,13 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DefaultWalletServerBinderTest { - private val userWalletsStore: UserWalletsStore = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk() private val appsFlyerStore: AppsFlyerStore = mockk() private val tangemTechApi: TangemTechApi = mockk() private val dispatchers: CoroutineDispatcherProvider = TestingCoroutineDispatcherProvider() private val binder = DefaultWalletServerBinder( - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, appsFlyerStore = appsFlyerStore, tangemTechApi = tangemTechApi, dispatchers = dispatchers @@ -47,7 +48,7 @@ internal class DefaultWalletServerBinderTest { @AfterEach fun tearDown() { - clearMocks(userWalletsStore, appsFlyerStore, tangemTechApi) + clearMocks(userWalletsListRepository, appsFlyerStore, tangemTechApi) } @Nested @@ -65,7 +66,9 @@ internal class DefaultWalletServerBinderTest { ) val apiResponse = ApiResponse.Success(Unit) - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { appsFlyerStore.get() } returns conversionData coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse @@ -74,23 +77,23 @@ internal class DefaultWalletServerBinderTest { Truth.assertThat(actual).isEqualTo(apiResponse) coVerifyOrder { - userWalletsStore.getSyncOrNull(userWalletId) + userWalletsListRepository.userWallets appsFlyerStore.get() tangemTechApi.createWallet(requestBody) } } @Test - fun `bind will skipped if userWalletsStore returns null`() = runTest { - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns null + fun `bind will skipped if userWalletsListRepository returns null`() = runTest { + val userWalletsFlow = MutableStateFlow?>(null) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val actual = binder.bind(userWalletId = userWalletId) Truth.assertThat(actual).isEqualTo(null) - coVerifyOrder { - userWalletsStore.getSyncOrNull(userWalletId) - } + coVerifyOrder { userWalletsListRepository.userWallets } coVerify(inverse = true) { appsFlyerStore.get() @@ -107,7 +110,9 @@ internal class DefaultWalletServerBinderTest { ) val apiResponse = ApiResponse.Success(Unit) - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { appsFlyerStore.get() } returns null coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse @@ -116,7 +121,7 @@ internal class DefaultWalletServerBinderTest { Truth.assertThat(actual).isEqualTo(apiResponse) coVerifyOrder { - userWalletsStore.getSyncOrNull(userWalletId) + userWalletsListRepository.userWallets appsFlyerStore.get() tangemTechApi.createWallet(requestBody) } @@ -133,7 +138,9 @@ internal class DefaultWalletServerBinderTest { ) val apiResponse = ApiResponse.Error(ApiResponseError.TimeoutException()) as ApiResponse - coEvery { userWalletsStore.getSyncOrNull(userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { appsFlyerStore.get() } returns conversionData coEvery { tangemTechApi.createWallet(requestBody) } returns apiResponse @@ -142,7 +149,7 @@ internal class DefaultWalletServerBinderTest { Truth.assertThat(actual).isEqualTo(apiResponse) coVerifyOrder { - userWalletsStore.getSyncOrNull(userWalletId) + userWalletsListRepository.userWallets appsFlyerStore.get() tangemTechApi.createWallet(requestBody) } 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/EarnFilterStore.kt b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnFilterStore.kt new file mode 100644 index 0000000000..3edbae153d --- /dev/null +++ b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnFilterStore.kt @@ -0,0 +1,17 @@ +package com.tangem.data.earn.datastore + +import com.tangem.datasource.local.datastore.RuntimeStateStore +import com.tangem.domain.earn.model.EarnFilter +import com.tangem.domain.earn.model.EarnFilterNetwork +import com.tangem.domain.earn.model.EarnFilterType +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class EarnFilterStore @Inject constructor() : + RuntimeStateStore by RuntimeStateStore( + defaultValue = EarnFilter( + earnFilterNetwork = EarnFilterNetwork.AllNetworks(isSelected = true), + earnFilterType = EarnFilterType.ALL, + ), + ) \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnNetworksStore.kt new file mode 100644 index 0000000000..c623e841eb --- /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 +internal class EarnNetworksStore @Inject constructor() : + RuntimeStateStore by RuntimeStateStore( + defaultValue = null, + ) \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt b/data/earn/src/main/java/com/tangem/data/earn/datastore/EarnTopTokensStore.kt new file mode 100644 index 0000000000..73e05f6289 --- /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 +internal class EarnTopTokensStore @Inject constructor() : + RuntimeStateStore by RuntimeStateStore( + defaultValue = null, + ) \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt b/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt new file mode 100644 index 0000000000..45e45e897d --- /dev/null +++ b/data/earn/src/main/java/com/tangem/data/earn/di/EarnDataModule.kt @@ -0,0 +1,50 @@ +package com.tangem.data.earn.di + +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.earn.DefaultEarnErrorResolver +import com.tangem.data.earn.datastore.EarnFilterStore +import com.tangem.data.earn.datastore.EarnNetworksStore +import com.tangem.data.earn.datastore.EarnTopTokensStore +import com.tangem.data.earn.repository.DefaultEarnRepository +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, + earnFilterStore: EarnFilterStore, + earnErrorResolver: EarnErrorResolver, + excludedBlockchains: ExcludedBlockchains, + ): EarnRepository { + return DefaultEarnRepository( + tangemTechApi = tangemTechApi, + dispatchers = dispatchers, + userWalletsListRepository = userWalletsListRepository, + earnNetworksStore = earnNetworksStore, + earnTopTokensStore = earnTopTokensStore, + earnErrorResolver = earnErrorResolver, + excludedBlockchains = excludedBlockchains, + earnFilterStore = earnFilterStore, + ) + } +} \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt b/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt new file mode 100644 index 0000000000..95deebdb54 --- /dev/null +++ b/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt @@ -0,0 +1,178 @@ +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.EarnFilterStore +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.EarnFilter +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 earnFilterStore: EarnFilterStore, + 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() { + runCatching(dispatchers.io) { + val response = tangemTechApi.getEarnNetworks().getOrThrow() + val items = response.items.map { dto -> + val blockchain = Blockchain.fromNetworkId(dto.networkId) + EarnNetwork( + networkId = dto.networkId, + isAdded = false, + fullName = blockchain?.fullName.orEmpty(), + symbol = blockchain?.currency.orEmpty(), + ) + } + earnNetworksStore.store(Either.Right(items)) + }.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() + } + + override fun observeEarnFilter(): Flow { + return earnFilterStore.get() + } + + override suspend fun setEarnFilter(filter: EarnFilter) { + earnFilterStore.store(filter) + } + + companion object { + internal const val FIRST_PAGE = 1 + private const val INITIAL_BATCH_KEY = 0 + } +} + +/* +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 + val address = earnToken.token.address + return if (address == null) { + 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 = 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..aeead13270 --- /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, + networks = params.networks, + ).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/express/build.gradle.kts b/data/express/build.gradle.kts index 8051766229..f75d1dde55 100644 --- a/data/express/build.gradle.kts +++ b/data/express/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(projects.data.common) /** Domain */ + implementation(projects.domain.common) implementation(projects.domain.express.models) implementation(projects.domain.express) implementation(projects.domain.wallets.models) diff --git a/data/express/src/main/java/com/tangem/data/express/DefaultExpressServiceFetcher.kt b/data/express/src/main/java/com/tangem/data/express/DefaultExpressServiceFetcher.kt index e794c648b0..6e97e2c7b5 100644 --- a/data/express/src/main/java/com/tangem/data/express/DefaultExpressServiceFetcher.kt +++ b/data/express/src/main/java/com/tangem/data/express/DefaultExpressServiceFetcher.kt @@ -10,7 +10,8 @@ import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.exchangeservice.swap.ExpressUtils.getRefCode import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.ExpressAssetsStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.catchOn import com.tangem.domain.core.utils.lceContent @@ -42,7 +43,7 @@ internal class DefaultExpressServiceFetcher @Inject constructor( private val tangemExpressApi: TangemExpressApi, private val expressAssetsStore: ExpressAssetsStore, private val appPreferencesStore: AppPreferencesStore, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val dispatchers: CoroutineDispatcherProvider, ) : ExpressServiceFetcher { @@ -52,7 +53,7 @@ internal class DefaultExpressServiceFetcher @Inject constructor( override suspend fun fetch(userWalletId: UserWalletId, assetIds: Set): Either = either { val userWallet = arrow.core.raise.catch( - block = { userWalletsStore.getSyncStrict(userWalletId) }, + block = { userWalletsListRepository.getSyncStrict(userWalletId) }, catch = ::raise, ) diff --git a/data/express/src/main/java/com/tangem/data/express/converter/ExpressProviderConverter.kt b/data/express/src/main/java/com/tangem/data/express/converter/ExpressProviderConverter.kt index 09a2f55b15..94d78677bb 100644 --- a/data/express/src/main/java/com/tangem/data/express/converter/ExpressProviderConverter.kt +++ b/data/express/src/main/java/com/tangem/data/express/converter/ExpressProviderConverter.kt @@ -20,6 +20,7 @@ internal class ExpressProviderConverter : Converter() + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.Q) override fun isWalletCreationSupported(): Boolean { return BuildConfig.DEBUG || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q @@ -34,4 +38,70 @@ internal class DefaultHotWalletRepository( ) } } + + override fun shouldShowUpgradeBanner(userWalletId: UserWalletId): Flow = appPreferencesStore + .getObjectMap(PreferencesKeys.SHOULD_SHOW_UPGRADE_BANNER_KEY) + .map { it[userWalletId.stringValue] == true } + + override suspend fun setShouldShowUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) { + appPreferencesStore.editData { mutablePreferences -> + mutablePreferences.setObjectMap( + key = PreferencesKeys.SHOULD_SHOW_UPGRADE_BANNER_KEY, + value = mutablePreferences.getObjectMap(PreferencesKeys.SHOULD_SHOW_UPGRADE_BANNER_KEY) + .plus(userWalletId.stringValue to shouldShow), + ) + } + } + + override fun upgradeBannerClosureTimestamp(userWalletId: UserWalletId): Flow = appPreferencesStore + .getObjectMap(PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY) + .map { it[userWalletId.stringValue] } + + override suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long?) { + appPreferencesStore.editData { mutablePreferences -> + mutablePreferences.setObjectMap( + key = PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY, + value = mutablePreferences.getObjectMap(PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY) + .plus(userWalletId.stringValue to timestamp), + ) + } + } + + override suspend fun getWalletCreationTimestamp(userWalletId: UserWalletId): Long? { + return appPreferencesStore + .getObjectMapSync(PreferencesKeys.WALLET_CREATION_TIMESTAMP_KEY)[userWalletId.stringValue] + } + + override suspend fun setWalletCreationTimestamp(userWalletId: UserWalletId, timestamp: Long) { + appPreferencesStore.editData { mutablePreferences -> + mutablePreferences.setObjectMap( + key = PreferencesKeys.WALLET_CREATION_TIMESTAMP_KEY, + value = mutablePreferences.getObjectMap(PreferencesKeys.WALLET_CREATION_TIMESTAMP_KEY) + .plus(userWalletId.stringValue to timestamp), + ) + } + } + + override suspend fun hasHadFirstTopUp(userWalletId: UserWalletId): Boolean { + return appPreferencesStore + .getObjectMapSync(PreferencesKeys.HAS_HAD_FIRST_TOP_UP_KEY)[userWalletId.stringValue] == true + } + + override suspend fun setHasHadFirstTopUp(userWalletId: UserWalletId, hasTopUp: Boolean) { + appPreferencesStore.editData { mutablePreferences -> + mutablePreferences.setObjectMap( + key = PreferencesKeys.HAS_HAD_FIRST_TOP_UP_KEY, + value = mutablePreferences.getObjectMap(PreferencesKeys.HAS_HAD_FIRST_TOP_UP_KEY) + .plus(userWalletId.stringValue to hasTopUp), + ) + } + } + + override fun isFirstTopUpDetectedThisSession(userWalletId: UserWalletId): Boolean { + return firstTopUpDetectedThisSession.containsKey(userWalletId.stringValue) + } + + override fun markFirstTopUpDetectedThisSession(userWalletId: UserWalletId) { + firstTopUpDetectedThisSession[userWalletId.stringValue] = Unit + } } \ No newline at end of file diff --git a/data/manage-tokens/detekt-baseline-debug.xml b/data/manage-tokens/detekt-baseline-debug.xml deleted file mode 100644 index a9b6d430bb..0000000000 --- a/data/manage-tokens/detekt-baseline-debug.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - MultilineLambdaItParameter:DefaultCustomTokensRepository.kt$DefaultCustomTokensRepository${ // TODO: refactor https://tangem.atlassian.net/browse/AND-10006\ if (it.isTestnet() || it in excludedBlockchains || it in hotWalletExcludedBlockchains) { return@mapNotNull null } networkFactory.create( blockchain = it, extraDerivationPath = null, userWallet = userWallet, ) } - MultilineLambdaItParameter:ManageTokensUpdateFetcher.kt$ManageTokensUpdateFetcher${ if (it.key == toUpdate[index].key) { Batch(it.key, updatedItems) } else { null } } - UnsafeCallOnNullableType:DefaultCustomTokensRepository.kt$DefaultCustomTokensRepository$coinNetwork.decimalCount!! - - diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index 1570f9a7c7..7725874819 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -13,11 +13,12 @@ import com.tangem.data.managetokens.utils.TokenAddressesConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.extensions.canHandleBlockchain import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains import com.tangem.domain.card.common.extensions.supportedBlockchains import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.managetokens.model.AddCustomTokenForm import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.repository.CustomTokensRepository @@ -32,7 +33,7 @@ import kotlinx.coroutines.withContext @Suppress("LongParameterList") internal class DefaultCustomTokensRepository( private val tangemTechApi: TangemTechApi, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val userTokensResponseStore: UserTokensResponseStore, private val walletManagersFacade: WalletManagersFacade, private val excludedBlockchains: ExcludedBlockchains, @@ -96,9 +97,8 @@ internal class DefaultCustomTokensRepository( networkId: Network.ID, derivationPath: Network.DerivationPath, ): CryptoCurrency.Token? = withContext(dispatchers.io) { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "User wallet [$userWalletId] not found while finding token" - } + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) + val network = requireNotNull( networkFactory.create( networkId = networkId, @@ -138,7 +138,7 @@ internal class DefaultCustomTokensRepository( rawId = CryptoCurrency.RawID(coin.id), name = coin.name, symbol = coin.symbol, - decimals = coinNetwork.decimalCount!!.toInt(), + decimals = requireNotNull(coinNetwork.decimalCount).toInt(), contractAddress = tokenAddress, ) } else { @@ -152,9 +152,7 @@ internal class DefaultCustomTokensRepository( networkId: Network.ID, derivationPath: Network.DerivationPath, ): CryptoCurrency.Coin { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "User wallet [$userWalletId] not found while creating coin" - } + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) val network = requireNotNull( networkFactory.create( networkId = networkId, @@ -189,9 +187,7 @@ internal class DefaultCustomTokensRepository( derivationPath: Network.DerivationPath, formValues: AddCustomTokenForm.Validated.All, ): CryptoCurrency.Token { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "User wallet [$userWalletId] not found while creating custom token" - } + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) val network = requireNotNull( networkFactory.create( networkId = networkId, @@ -217,6 +213,7 @@ internal class DefaultCustomTokensRepository( ) } + @Deprecated("Use ManageCryptoCurrenciesUseCase") override suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) = withContext(dispatchers.io) { val cryptoCurrency = when (currency) { @@ -273,20 +270,15 @@ internal class DefaultCustomTokensRepository( } override suspend fun getSupportedNetworks(userWalletId: UserWalletId): List = withContext(dispatchers.io) { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "User wallet [$userWalletId] not found while getting supported networks" - } - - when (userWallet) { + when (val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)) { is UserWallet.Hot -> { - Blockchain.entries.mapNotNull { - // TODO: refactor [REDACTED_JIRA]\ - if (it.isTestnet() || it in excludedBlockchains || it in hotWalletExcludedBlockchains) { - return@mapNotNull null - } + Blockchain.entries.mapNotNull { blockchain -> + // TODO: refactor [REDACTED_JIRA] + val isExcluded = blockchain in excludedBlockchains || blockchain in hotWalletExcludedBlockchains + if (blockchain.isTestnet() || isExcluded) return@mapNotNull null networkFactory.create( - blockchain = it, + blockchain = blockchain, extraDerivationPath = null, userWallet = userWallet, ) diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index 84eea67a90..98f8097fbc 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -22,11 +22,12 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.orDefault import com.tangem.datasource.local.config.testnet.TestnetTokensStorage import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.TapWorkarounds.isTestCard import com.tangem.domain.card.common.extensions.* import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.managetokens.model.* import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork import com.tangem.domain.managetokens.repository.ManageTokensRepository @@ -46,7 +47,7 @@ import com.tangem.utils.coroutines.runSuspendCatching @Suppress("LongParameterList", "LargeClass") internal class DefaultManageTokensRepository( private val tangemTechApi: TangemTechApi, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val userTokenSaver: UserTokensSaver, private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher, private val userTokensResponseStore: UserTokensResponseStore, @@ -88,7 +89,7 @@ internal class DefaultManageTokensRepository( prefetchDistance = batchSize, batchSize = batchSize, subFetcher = { request, _, isFirstBatchFetching -> - val userWallet = request.params.userWalletId?.let(userWalletsStore::getSyncStrict) + val userWallet = request.params.userWalletId?.let(userWalletsListRepository::getSyncStrict) if (userWallet is UserWallet.Cold && userWallet.scanResponse.card.isTestCard) { when (val params = request.params) { @@ -438,7 +439,7 @@ internal class DefaultManageTokensRepository( userWalletId: UserWalletId, sourceNetwork: SourceNetwork, ): CurrencyUnsupportedState? { - val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = userWalletId) val blockchain = sourceNetwork.id.toBlockchain() return when (sourceNetwork) { @@ -452,7 +453,7 @@ internal class DefaultManageTokensRepository( rawNetworkId: String, isMainNetwork: Boolean, ): CurrencyUnsupportedState? { - val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = userWalletId) val blockchain = Blockchain.fromNetworkId(networkId = rawNetworkId) ?: error("Can not create blockchain with given networkId -> $rawNetworkId") return if (isMainNetwork) { diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index 97838b82c5..ea538bf3d2 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -11,8 +11,8 @@ import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.config.testnet.TestnetTokensStorage import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository import com.tangem.domain.walletmanager.WalletManagersFacade @@ -31,7 +31,7 @@ internal object ManageTokensDataModule { @Singleton fun provideManageTokensRepository( tangemTechApi: TangemTechApi, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, manageTokensUpdateFetcher: ManageTokensUpdateFetcher, userTokensResponseStore: UserTokensResponseStore, userTokensSaver: UserTokensSaver, @@ -45,7 +45,7 @@ internal object ManageTokensDataModule { ): ManageTokensRepository { return DefaultManageTokensRepository( tangemTechApi = tangemTechApi, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, manageTokensUpdateFetcher = manageTokensUpdateFetcher, userTokensResponseStore = userTokensResponseStore, userTokenSaver = userTokensSaver, @@ -63,7 +63,7 @@ internal object ManageTokensDataModule { @Singleton fun provideCustomTokensRepository( tangemTechApi: TangemTechApi, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, userTokensResponseStore: UserTokensResponseStore, walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, @@ -73,7 +73,7 @@ internal object ManageTokensDataModule { ): CustomTokensRepository { return DefaultCustomTokensRepository( tangemTechApi = tangemTechApi, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, userTokensResponseStore = userTokensResponseStore, walletManagersFacade = walletManagersFacade, excludedBlockchains = excludedBlockchains, diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt index 66bfc00bc5..8778b13b4d 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManageTokensUpdateFetcher.kt @@ -51,9 +51,9 @@ internal class ManageTokensUpdateFetcher @Inject constructor() : update { BatchUpdateResult.Success( - data = mapNotNull { - if (it.key == toUpdate[index].key) { - Batch(it.key, updatedItems) + data = mapNotNull { batch -> + if (batch.key == toUpdate[index].key) { + Batch(batch.key, updatedItems) } else { null } diff --git a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt index 9c1b3be3f1..12d5b0f694 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/DefaultMarketsTokenRepository.kt @@ -19,8 +19,9 @@ import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.markets.models.response.TokenMarketExchangesResponse import com.tangem.datasource.local.datastore.RuntimeStateStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.models.account.DerivationIndex @@ -38,7 +39,7 @@ import java.util.concurrent.atomic.AtomicLong internal class DefaultMarketsTokenRepository( private val marketsApi: TangemTechMarketsApi, private val quotesFetcher: QuotesFetcher, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val dispatcherProvider: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val cacheRegistry: CacheRegistry, @@ -74,6 +75,7 @@ internal class DefaultMarketsTokenRepository( offset = request.offset, limit = request.limit, timestamp = if (isFirstBatchFetching) null else requestTimeStamp.get(), + showNetworks = request.params.shouldNetworks, ).getOrThrow() } @@ -251,7 +253,7 @@ internal class DefaultMarketsTokenRepository( network: TokenMarketInfo.Network, accountIndex: DerivationIndex?, ): CryptoCurrency? { - val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("UserWalletId [$userWalletId] not found") + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) val blockchain = Blockchain.fromNetworkId(network.networkId) ?: error("Unknown network [${network.networkId}]") return if (network.contractAddress == null) { diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt index f43e7ddae1..86b29f8006 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -42,6 +42,13 @@ internal object TokenMarketListConverter : Converter + TokenMarket.Network( + networkId = network.networkId, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) + }, ) } return TokenMarketListWithMaxApy(tokens, value.summary?.maxApy) diff --git a/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt index 7632ec2cb1..1e216b4717 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/di/MarketsDataModule.kt @@ -8,7 +8,7 @@ import com.tangem.data.common.quote.QuotesFetcher import com.tangem.data.markets.DefaultMarketsTokenRepository import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.local.datastore.RuntimeStateStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -26,7 +26,7 @@ internal object MarketsDataModule { fun provideMarketsTokenRepository( marketsApi: TangemTechMarketsApi, quotesFetcher: QuotesFetcher, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, dispatchers: CoroutineDispatcherProvider, analyticsEventHandler: AnalyticsEventHandler, cacheRegistry: CacheRegistry, @@ -37,7 +37,7 @@ internal object MarketsDataModule { marketsApi = marketsApi, quotesFetcher = quotesFetcher, dispatcherProvider = dispatchers, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, analyticsEventHandler = analyticsEventHandler, cacheRegistry = cacheRegistry, tokenExchangesStore = RuntimeStateStore(defaultValue = emptyList()), diff --git a/data/networks/build.gradle.kts b/data/networks/build.gradle.kts index c61859dc69..90ca5576e0 100644 --- a/data/networks/build.gradle.kts +++ b/data/networks/build.gradle.kts @@ -24,11 +24,12 @@ dependencies { // endregion // region Project - Domain - implementation(projects.domain.legacy) - implementation(projects.domain.walletManager) implementation(projects.domain.card) - api(projects.domain.models) + implementation(projects.domain.common) + implementation(projects.domain.legacy) + implementation(projects.domain.models) implementation(projects.domain.networks) + implementation(projects.domain.walletManager) // endregion // region Project - Libs diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt index 895dc129a7..8406342ea9 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducer.kt @@ -4,7 +4,8 @@ import arrow.core.Option import arrow.core.some import com.tangem.data.common.network.NetworkFactory import com.tangem.data.networks.store.NetworksStatusesStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncOrNull import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.networks.multi.MultiNetworkStatusProducer @@ -28,7 +29,7 @@ internal class DefaultMultiNetworkStatusProducer @AssistedInject constructor( @Assisted val params: MultiNetworkStatusProducer.Params, override val flowProducerTools: FlowProducerTools, private val networksStatusesStore: NetworksStatusesStore, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val networkFactory: NetworkFactory, private val dispatchers: CoroutineDispatcherProvider, ) : MultiNetworkStatusProducer { @@ -39,7 +40,7 @@ internal class DefaultMultiNetworkStatusProducer @AssistedInject constructor( return networksStatusesStore.get(userWalletId = params.userWalletId) .distinctUntilChanged() .mapNotNull { statuses -> - val userWallet = userWalletsStore.getSyncOrNull(params.userWalletId) + val userWallet = userWalletsListRepository.getSyncOrNull(params.userWalletId) if (userWallet == null) { Timber.e("Unable to get UserWallet with provided ID: ${params.userWalletId}") diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt index aa5c37e306..4b35a6fa71 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusProducerTest.kt @@ -9,8 +9,9 @@ import com.tangem.data.common.network.NetworkFactory import com.tangem.data.networks.models.SimpleNetworkStatus import com.tangem.data.networks.store.NetworksStatusesStore import com.tangem.data.networks.toSimple -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.configs.GenericCardConfig +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncOrNull import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.networks.multi.MultiNetworkStatusProducer @@ -33,7 +34,7 @@ internal class DefaultMultiNetworkStatusProducerTest { private val params = MultiNetworkStatusProducer.Params(userWalletId = userWallet.walletId) private val networksStatusesStore = mockk() - private val userWalletsStore = mockk() + private val userWalletsListRepository = mockk() private val networkFactory = mockk() private val dispatchers = TestingCoroutineDispatcherProvider() private val flowProducerTools: FlowProducerTools = mockk() @@ -41,7 +42,7 @@ internal class DefaultMultiNetworkStatusProducerTest { private val producer = DefaultMultiNetworkStatusProducer( params = params, networksStatusesStore = networksStatusesStore, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, networkFactory = networkFactory, dispatchers = dispatchers, flowProducerTools = flowProducerTools, @@ -49,7 +50,7 @@ internal class DefaultMultiNetworkStatusProducerTest { @BeforeEach fun resetMocks() { - clearMocks(networksStatusesStore, userWalletsStore, networkFactory) + clearMocks(networksStatusesStore, userWalletsListRepository, networkFactory) } @Test @@ -65,7 +66,9 @@ internal class DefaultMultiNetworkStatusProducerTest { val networksStatusesFlow = flowOf(simpleStatuses) every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow - every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { networkFactory.create( @@ -94,7 +97,7 @@ internal class DefaultMultiNetworkStatusProducerTest { verifyOrder { networksStatusesStore.get(params.userWalletId) - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, @@ -129,7 +132,9 @@ internal class DefaultMultiNetworkStatusProducerTest { // region every every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow - every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { networkFactory.create( networkId = simpleStatuses.first().id, @@ -177,7 +182,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, @@ -202,7 +207,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual2).isEqualTo(expected2) verifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets networkFactory.create( networkId = updatedSimpleStatuses.first().id, derivationPath = updatedSimpleStatuses.first().id.derivationPath, @@ -230,7 +235,9 @@ internal class DefaultMultiNetworkStatusProducerTest { // region every every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow - every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { networkFactory.create( @@ -263,7 +270,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, @@ -312,7 +319,9 @@ internal class DefaultMultiNetworkStatusProducerTest { // region every every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow - every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow every { networkFactory.create( networkId = simpleStatuses.first().id, @@ -340,7 +349,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual1.first()).isEqualTo(expected1) verifyOrder(inverse = true) { - userWalletsStore.getSyncOrNull(any()) + userWalletsListRepository.getSyncOrNull(any()) networkFactory.create(networkId = any(), derivationPath = any(), userWallet = any()) } @@ -354,7 +363,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual2.first()).isEqualTo(expected2) verifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, @@ -382,7 +391,7 @@ internal class DefaultMultiNetworkStatusProducerTest { Truth.assertThat(actual.first()).isEqualTo(expected) verify { networksStatusesStore.get(params.userWalletId) } - verify(inverse = true) { userWalletsStore.getSyncOrNull(params.userWalletId) } + verify(inverse = true) { userWalletsListRepository.userWallets } } @Test @@ -398,7 +407,9 @@ internal class DefaultMultiNetworkStatusProducerTest { val networksStatusesFlow = flowOf(simpleStatuses) every { networksStatusesStore.get(params.userWalletId) } returns networksStatusesFlow - every { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { networkFactory.create(networkId = any(), any(), any()) } returns null // Act @@ -411,7 +422,7 @@ internal class DefaultMultiNetworkStatusProducerTest { verifyOrder { networksStatusesStore.get(params.userWalletId) - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets networkFactory.create( networkId = simpleStatuses.first().id, derivationPath = simpleStatuses.first().id.derivationPath, diff --git a/data/nft/build.gradle.kts b/data/nft/build.gradle.kts index 4124798b29..f53acc71ce 100644 --- a/data/nft/build.gradle.kts +++ b/data/nft/build.gradle.kts @@ -23,13 +23,14 @@ dependencies { implementation(projects.data.common) /** Project - Domain */ - implementation(projects.domain.models) - implementation(projects.domain.wallets.models) - implementation(projects.domain.tokens.models) - implementation(projects.domain.nft) - implementation(projects.domain.walletManager) implementation(projects.domain.card) + implementation(projects.domain.common) + implementation(projects.domain.models) + implementation(projects.domain.nft) implementation(projects.domain.nft.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.walletManager) + implementation(projects.domain.wallets.models) /** Project - Utils */ implementation(projects.core.utils) diff --git a/data/nft/detekt-baseline-debug.xml b/data/nft/detekt-baseline-debug.xml deleted file mode 100644 index d4e81aea8d..0000000000 --- a/data/nft/detekt-baseline-debug.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ NFTCollections( network = network, content = NFTCollections.Content.Collections( collections = it ?.map { collection -> nftSdkCollectionConverter.convert(network to collection) } ?.filter { it.id !is NFTCollection.Identifier.Unknown }, source = StatusSource.CACHE, ), ) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ if (it !is UnsupportedOperationException) { saveFailedStateInRuntime( userWalletId = userWalletId, network = network, error = it, ) } } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ if (it.id == collectionId) { it.changeAssetsStatusSource(source) } else { it } } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ if (it.identifier == sdkCollectionId) { it.copy(assets = assets) } else { it } } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ networkFactory.create( blockchain = it, extraDerivationPath = null, userWallet = userWallet, ) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ nftRuntimeStores[storeId] = it val storedCollections = getStoredCollections(userWalletId, network) val storedPrices = getStoredPrices(userWalletId, network) it.initialize( collections = storedCollections, prices = storedPrices, ) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ saveCollectionsInRuntime( userWalletId = userWalletId, network = network, collections = it, ) saveCollectionsInPersistence( userWalletId = userWalletId, network = network, collections = it, ) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ saveFailedStateInRuntime( userWalletId = userWalletId, network = network, error = it, ) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ storedCollections.copy( content = it, ) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ val (assetId, _) = it assetIdConverter.convert(assetId) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ val (assetId, price) = it val nftCurrency = getNFTCurrency(network) NFTSalePrice.Value( assetId = assetId, value = price.value, fiatValue = null, symbol = nftCurrency.symbol, decimals = nftCurrency.decimals, ) } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ val assetId = assetIdConverter.convert(it.identifier) val price = getNFTRuntimeStore(userWalletId, network).getSalePriceSync(assetId) if (price is NFTSalePrice.Empty || price is NFTSalePrice.Error) { refreshSalePrice(userWalletId, network, sdkCollectionId, it.identifier) } } - MultilineLambdaItParameter:DefaultNFTRepository.kt$DefaultNFTRepository${ val sdkAssetId = assetIdConverter.convertBack(assetId) saveSalePriceInPersistence(userWalletId, network, sdkAssetId, it) } - NamedArguments:DefaultNFTRepository.kt$DefaultNFTRepository$refreshSalePrice(userWalletId, network, sdkCollectionId, it.identifier) - NamedArguments:DefaultNFTRepository.kt$DefaultNFTRepository$saveSalePriceInPersistence(userWalletId, network, sdkAssetId, it) - NoNameShadowing:DefaultNFTRepository.kt$DefaultNFTRepository${ it.id !is NFTCollection.Identifier.Unknown } - SuspendFunSwallowedCancellation:DefaultNFTRepository.kt$DefaultNFTRepository$runCatching - SuspendFunWithFlowReturnType:DefaultNFTRepository.kt$DefaultNFTRepository$suspend - UnnecessaryLet:DefaultNFTRepository.kt$DefaultNFTRepository$let { prices -> prices .mapKeys { val (assetId, _) = it assetIdConverter.convert(assetId) } .mapValues { val (assetId, price) = it val nftCurrency = getNFTCurrency(network) NFTSalePrice.Value( assetId = assetId, value = price.value, fiatValue = null, symbol = nftCurrency.symbol, decimals = nftCurrency.decimals, ) } } - - diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index 851849db2a..7b022349ef 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -16,8 +16,9 @@ import com.tangem.datasource.local.nft.converter.NFTSdkAssetIdentifierConverter import com.tangem.datasource.local.nft.converter.NFTSdkAssetSalePriceConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionConverter import com.tangem.datasource.local.nft.converter.NFTSdkCollectionIdentifierConverter -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.extensions.canHandleToken +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.models.StatusSource import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -47,13 +48,13 @@ import javax.inject.Inject import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection -@Suppress("LargeClass", "LongParameterList") +@Suppress("LargeClass", "LongParameterList", "SuspendFunWithFlowReturnType") internal class DefaultNFTRepository @Inject constructor( private val nftPersistenceStoreFactory: NFTPersistenceStoreFactory, private val nftRuntimeStoreFactory: NFTRuntimeStoreFactory, private val walletManagersFacade: WalletManagersFacade, private val dispatchers: CoroutineDispatcherProvider, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val networkFactory: NetworkFactory, private val excludedBlockchains: ExcludedBlockchains, @ApplicationContext private val context: Context, @@ -87,7 +88,7 @@ internal class DefaultNFTRepository @Inject constructor( ): NFTSalePrice = withContext(dispatchers.io) { val salePriceConverter = NFTSdkAssetSalePriceConverter(assetId) - runCatching { + runSuspendCatching { saveSalePriceInRuntime(userWalletId, network, NFTSalePrice.Loading(assetId)) val sdkPrice = walletManagersFacade.getNFTSalePrice( @@ -108,9 +109,14 @@ internal class DefaultNFTRepository @Inject constructor( saveSalePriceInRuntime(userWalletId, network, salePrice) - sdkPrice?.let { + sdkPrice?.let { price -> val sdkAssetId = assetIdConverter.convertBack(assetId) - saveSalePriceInPersistence(userWalletId, network, sdkAssetId, it) + saveSalePriceInPersistence( + userWalletId = userWalletId, + network = network, + assetId = sdkAssetId, + salePrice = price, + ) } salePrice @@ -162,41 +168,46 @@ internal class DefaultNFTRepository @Inject constructor( expireAssets(userWalletId, network, collectionId) - assets.forEach { - val assetId = assetIdConverter.convert(it.identifier) + assets.forEach { asset -> + val assetId = assetIdConverter.convert(asset.identifier) val price = getNFTRuntimeStore(userWalletId, network).getSalePriceSync(assetId) if (price is NFTSalePrice.Empty || price is NFTSalePrice.Error) { - refreshSalePrice(userWalletId, network, sdkCollectionId, it.identifier) + refreshSalePrice( + userWalletId = userWalletId, + network = network, + sdkCollectionId = sdkCollectionId, + sdkAssetId = asset.identifier, + ) } } getNFTPersistenceStore(userWalletId, network) .getCollectionsSync() - ?.map { - if (it.identifier == sdkCollectionId) { - it.copy(assets = assets) + ?.map { collection -> + if (collection.identifier == sdkCollectionId) { + collection.copy(assets = assets) } else { - it + collection } } - ?.let { + ?.let { collections -> saveCollectionsInRuntime( userWalletId = userWalletId, network = network, - collections = it, + collections = collections, ) saveCollectionsInPersistence( userWalletId = userWalletId, network = network, - collections = it, + collections = collections, ) } - }.onLeft { - if (it !is UnsupportedOperationException) { + }.onLeft { throwable -> + if (throwable !is UnsupportedOperationException) { saveFailedStateInRuntime( userWalletId = userWalletId, network = network, - error = it, + error = throwable, ) } } @@ -207,13 +218,13 @@ internal class DefaultNFTRepository @Inject constructor( network.canHandleNFTs(userWalletId) override suspend fun getNFTSupportedNetworks(userWalletId: UserWalletId): List { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) return Blockchain .entries .filter { !it.isTestnet() } - .mapNotNull { + .mapNotNull { blockchain -> networkFactory.create( - blockchain = it, + blockchain = blockchain, extraDerivationPath = null, userWallet = userWallet, ) @@ -280,11 +291,11 @@ internal class DefaultNFTRepository @Inject constructor( ) } } - }.onLeft { + }.onLeft { throwable -> saveFailedStateInRuntime( userWalletId = userWalletId, network = network, - error = it, + error = throwable, ) } }.saveIn(getNetworkJobHolder(network)) @@ -334,11 +345,7 @@ internal class DefaultNFTRepository @Inject constructor( val storedCollections = runtimeStore.getCollectionsSync() val expiredCollections = storedCollections .changeCollectionAssetsStatusSource(collectionId, StatusSource.CACHE) - .let { - storedCollections.copy( - content = it, - ) - } + .let { storedCollections.copy(content = it) } runtimeStore.saveCollections(expiredCollections) } @@ -427,6 +434,7 @@ internal class DefaultNFTRepository @Inject constructor( } } + // TODO: remove suspend private suspend fun getNFTRuntimeStore(userWalletId: UserWalletId, network: Network): NFTRuntimeStore { val storeId = (userWalletId to network).formatted() return nftRuntimeStoresMutex.withLock { @@ -446,44 +454,41 @@ internal class DefaultNFTRepository @Inject constructor( private suspend fun getStoredCollections(userWalletId: UserWalletId, network: Network) = getNFTPersistenceStore(userWalletId, network) .getCollectionsSync() - .let { + .let { collections -> NFTCollections( network = network, content = NFTCollections.Content.Collections( - collections = it + collections = collections ?.map { collection -> nftSdkCollectionConverter.convert(network to collection) } - ?.filter { - it.id !is NFTCollection.Identifier.Unknown - }, + ?.filter { it.id !is NFTCollection.Identifier.Unknown }, source = StatusSource.CACHE, ), ) } - private suspend fun getStoredPrices(userWalletId: UserWalletId, network: Network) = - getNFTPersistenceStore(userWalletId, network) + private suspend fun getStoredPrices( + userWalletId: UserWalletId, + network: Network, + ): Map { + val prices = getNFTPersistenceStore(userWalletId, network) .getSalePricesSync() .orEmpty() - .let { prices -> - prices - .mapKeys { - val (assetId, _) = it - assetIdConverter.convert(assetId) - } - .mapValues { - val (assetId, price) = it - val nftCurrency = getNFTCurrency(network) - NFTSalePrice.Value( - assetId = assetId, - value = price.value, - fiatValue = null, - symbol = nftCurrency.symbol, - decimals = nftCurrency.decimals, - ) - } + + return prices + .mapKeys { (assetId, _) -> assetIdConverter.convert(assetId) } + .mapValues { (assetId, price) -> + val nftCurrency = getNFTCurrency(network) + NFTSalePrice.Value( + assetId = assetId, + value = price.value, + fiatValue = null, + symbol = nftCurrency.symbol, + decimals = nftCurrency.decimals, + ) } + } private fun NFTCollections.changeStatusSource(source: StatusSource) = copy( content = when (val content = content) { @@ -503,11 +508,11 @@ internal class DefaultNFTRepository @Inject constructor( .copy( collections = content .collections - ?.map { - if (it.id == collectionId) { - it.changeAssetsStatusSource(source) + ?.map { collection -> + if (collection.id == collectionId) { + collection.changeAssetsStatusSource(source) } else { - it + collection } }, ) @@ -562,7 +567,7 @@ internal class DefaultNFTRepository @Inject constructor( } private fun Network.canHandleNFTs(userWalletId: UserWalletId): Boolean { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) val blockchain = Blockchain.fromNetworkId(backendId) ?: return false return blockchain.canHandleNFTs() && diff --git a/data/nft/src/test/kotlin/com/tangem/data/nft/NFTCleanerTest.kt b/data/nft/src/test/kotlin/com/tangem/data/nft/NFTCleanerTest.kt index 1a27526992..5056f8420f 100644 --- a/data/nft/src/test/kotlin/com/tangem/data/nft/NFTCleanerTest.kt +++ b/data/nft/src/test/kotlin/com/tangem/data/nft/NFTCleanerTest.kt @@ -24,7 +24,7 @@ class NFTCleanerTest { nftRuntimeStoreFactory = nftRuntimeStoreFactory, walletManagersFacade = mockk(), dispatchers = mockk(), - userWalletsStore = mockk(), + userWalletsListRepository = mockk(), networkFactory = mockk(), excludedBlockchains = mockk(), context = mockk(), diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt index e98f62d817..34a6666a6c 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultPushNotificationsRepository.kt @@ -15,10 +15,14 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.models.NotificationsError import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.domain.notifications.models.NotificationsEligibleNetwork +import com.tangem.datasource.local.preferences.utils.getObjectMap +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.info.AppInfoProvider import kotlinx.coroutines.withContext @@ -96,4 +100,9 @@ internal class DefaultPushNotificationsRepository @Inject constructor( NotificationsEligibleNetworkConverter.convert(it) } } + + override suspend fun isNotificationsEnabled(userWalletId: UserWalletId): Boolean = + appPreferencesStore.getObjectMap(PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY) + .map { it[userWalletId.stringValue] == true } + .firstOrNull() == true } \ No newline at end of file diff --git a/data/onramp/detekt-baseline-debug.xml b/data/onramp/detekt-baseline-debug.xml deleted file mode 100644 index ec3fa17add..0000000000 --- a/data/onramp/detekt-baseline-debug.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - BooleanPropertyNaming:DefaultOnrampRepository.kt$DefaultOnrampRepository$val cachedValue = onrampSepaAvailabilityStore.getSyncOrNull(key) - MultilineLambdaItParameter:DefaultHotCryptoRepository.kt$DefaultHotCryptoRepository${ Timber.e(it, "Unable to fetch hot crypto") analyticsEventHandler.send( event = MainScreenAnalyticsEvent.HotTokenError( errorCode = (it as? ApiResponseError.HttpException)?.code?.numericCode?.toString().orEmpty(), ), ) } - MultilineLambdaItParameter:DefaultHotCryptoRepository.kt$DefaultHotCryptoRepository${ if (it.id == OLD_POLYGON_NAME) { it.copy(id = NEW_POLYGON_NAME) } else { it } } - MultilineLambdaItParameter:DefaultHotCryptoRepository.kt$DefaultHotCryptoRepository${ val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("UserWalletId [$userWalletId] not found") HotCryptoCurrencyConverter( userWallet = userWallet, imageHost = it.imageHost, excludedBlockchains = excludedBlockchains, ) .convertList(input = it.tokens) .filterNotNull() } - MultilineLambdaItParameter:DefaultOnrampRepository.kt$DefaultOnrampRepository${ Timber.w(it, "Unable to fetch express providers") throw it } - MultilineLambdaItParameter:DefaultOnrampRepository.kt$DefaultOnrampRepository${ Timber.w(it, "Unable to fetch onramp pairs") throw it } - MultilineLambdaItParameter:DefaultOnrampRepository.kt$DefaultOnrampRepository${ Timber.w(it, "Unable to fetch onramp payment methods") throw it } - UnnecessaryLet:DefaultOnrampRepository.kt$DefaultOnrampRepository$let(countryConverter::convert) - UnnecessaryLet:DefaultOnrampRepository.kt$DefaultOnrampRepository$let(paymentMethodsConverter::convertList) - UnnecessaryLet:DefaultOnrampRepository.kt$DefaultOnrampRepository$let(statusConverter::convert) - UseOrEmpty:DefaultOnrampTransactionRepository.kt$DefaultOnrampTransactionRepository$stored ?: emptySet() - - diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt index 5b985a43fc..6501990863 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -18,10 +18,12 @@ import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.extensions.canHandleBlockchain import com.tangem.domain.card.common.extensions.canHandleToken +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict +import com.tangem.domain.common.wallets.loadAndGet import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.HotCryptoCurrency @@ -54,7 +56,7 @@ import timber.log.Timber internal class DefaultHotCryptoRepository( private val excludedBlockchains: ExcludedBlockchains, private val hotCryptoResponseStore: HotCryptoResponseStore, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val tangemTechApi: TangemTechApi, private val appCurrencyResponseStore: AppCurrencyResponseStore, private val accountsFeatureToggles: AccountsFeatureToggles, @@ -88,22 +90,21 @@ internal class DefaultHotCryptoRepository( return hotCryptoResponseStore.get() .map { it[userWalletId] } .filterNotNull() - .map { - val userWallet = userWalletsStore.getSyncOrNull(userWalletId) - ?: error("UserWalletId [$userWalletId] not found") + .map { response -> + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) HotCryptoCurrencyConverter( userWallet = userWallet, - imageHost = it.imageHost, + imageHost = response.imageHost, excludedBlockchains = excludedBlockchains, ) - .convertList(input = it.tokens) + .convertList(input = response.tokens) .filterNotNull() } } private fun getWalletsWithTokensFlow(): Flow>> { - return userWalletsStore.userWallets.flatMapLatest { userWallets -> + return userWalletsListRepository.loadAndGet().flatMapLatest { userWallets -> val flows = userWallets.map { userWallet -> if (accountsFeatureToggles.isFeatureEnabled) { walletAccountsFetcher.get(userWalletId = userWallet.walletId).map { it.toUserTokensResponse() } @@ -131,12 +132,13 @@ internal class DefaultHotCryptoRepository( tangemTechApi.getHotCrypto(currencyId = appCurrencyId).getOrThrow() } .onSuccess { Timber.d("HotCrypto is successfully updated") } - .onFailure { - Timber.e(it, "Unable to fetch hot crypto") + .onFailure { throwable -> + Timber.e(throwable, "Unable to fetch hot crypto") + val httpException = throwable as? ApiResponseError.HttpException analyticsEventHandler.send( event = MainScreenAnalyticsEvent.HotTokenError( - errorCode = (it as? ApiResponseError.HttpException)?.code?.numericCode?.toString().orEmpty(), + errorCode = httpException?.code?.numericCode?.toString().orEmpty(), ), ) } @@ -164,11 +166,11 @@ internal class DefaultHotCryptoRepository( } private fun List.applyTokensIdMigrations(): List { - return this.map { - if (it.id == OLD_POLYGON_NAME) { - it.copy(id = NEW_POLYGON_NAME) + return this.map { token -> + if (token.id == OLD_POLYGON_NAME) { + token.copy(id = NEW_POLYGON_NAME) } else { - it + token } } } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt index be4b3d4460..bf07c9a118 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt @@ -138,7 +138,7 @@ internal class DefaultOnrampRepository( } } - val country = onrampApi.getCountryByIp( + val response = onrampApi.getCountryByIp( userWalletId = userWallet.walletId.stringValue, refCode = ExpressUtils.getRefCode( userWallet = userWallet, @@ -146,7 +146,8 @@ internal class DefaultOnrampRepository( ), ) .getOrThrow() - .let(countryConverter::convert) + + val country = countryConverter.convert(response) onrampCurrentCountryByIPStore.store(country) @@ -154,7 +155,7 @@ internal class DefaultOnrampRepository( } override suspend fun getStatus(userWallet: UserWallet, txId: String): OnrampStatus = withContext(dispatchers.io) { - onrampApi.getStatus( + val response = onrampApi.getStatus( userWalletId = userWallet.walletId.stringValue, refCode = ExpressUtils.getRefCode( userWallet = userWallet, @@ -163,7 +164,8 @@ internal class DefaultOnrampRepository( txId = txId, ) .getOrThrow() - .let(statusConverter::convert) + + statusConverter.convert(response) } override suspend fun saveDefaultCurrency(currency: OnrampCurrency) = withContext(dispatchers.io) { @@ -218,9 +220,9 @@ internal class DefaultOnrampRepository( ), ).bind() }, - onError = { - Timber.w(it, "Unable to fetch onramp payment methods") - throw it + onError = { error -> + Timber.w(error, "Unable to fetch onramp payment methods") + throw error }, ) paymentMethodsStore.store(PAYMENT_METHODS_KEY, response.removeApplePay()) @@ -253,9 +255,9 @@ internal class DefaultOnrampRepository( ), ).bind() }, - onError = { - Timber.w(it, "Unable to fetch onramp pairs") - throw it + onError = { error -> + Timber.w(error, "Unable to fetch onramp pairs") + throw error }, ) } @@ -270,9 +272,9 @@ internal class DefaultOnrampRepository( ), ).bind() }, - onError = { - Timber.w(it, "Unable to fetch express providers") - throw it + onError = { error -> + Timber.w(error, "Unable to fetch express providers") + throw error }, ) } @@ -291,10 +293,10 @@ internal class DefaultOnrampRepository( cryptoCurrency = cryptoCurrency, ) - val cachedValue = onrampSepaAvailabilityStore.getSyncOrNull(key) + val isCachedValue = onrampSepaAvailabilityStore.getSyncOrNull(key) - if (cachedValue != null) { - return@withContext cachedValue + if (isCachedValue != null) { + return@withContext isCachedValue } val onrampPairs = @@ -318,9 +320,9 @@ internal class DefaultOnrampRepository( ), ).bind() }, - onError = { - Timber.w(it, "Unable to fetch onramp pairs") - throw it + onError = { error -> + Timber.w(error, "Unable to fetch onramp pairs") + throw error }, ) @@ -525,9 +527,11 @@ internal class DefaultOnrampRepository( } private suspend fun getPaymentMethods(): List { - return requireNotNull(paymentMethodsStore.getSyncOrNull(PAYMENT_METHODS_KEY)) { + val methods = requireNotNull(paymentMethodsStore.getSyncOrNull(PAYMENT_METHODS_KEY)) { "Onramp payment methods is absent in storage" - }.let(paymentMethodsConverter::convertList) + } + + return paymentMethodsConverter.convertList(methods) } private fun createOnrampTransaction( diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt index 782b4b7b5b..77c3740a75 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampTransactionRepository.kt @@ -100,7 +100,7 @@ internal class DefaultOnrampTransactionRepository( mutablePreferences.setObjectSet( key = PreferencesKeys.ONRAMP_TRANSACTIONS_STATUSES_KEY, - value = stored ?: emptySet(), + value = stored.orEmpty(), ) } } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index a4e93fec4f..88a6e8f021 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -28,8 +28,8 @@ import com.tangem.datasource.local.onramp.sepa.OnrampCurrentCountryByIPStore import com.tangem.datasource.local.onramp.sepa.OnrampSepaAvailabilityStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.onramp.repositories.* import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -105,7 +105,7 @@ internal object OnrampDataModule { fun provideHotCryptoRepository( excludedBlockchains: ExcludedBlockchains, hotCryptoResponseStore: HotCryptoResponseStore, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, tangemTechApi: TangemTechApi, appCurrencyResponseStore: AppCurrencyResponseStore, dispatchers: CoroutineDispatcherProvider, @@ -117,7 +117,7 @@ internal object OnrampDataModule { return DefaultHotCryptoRepository( excludedBlockchains = excludedBlockchains, hotCryptoResponseStore = hotCryptoResponseStore, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, tangemTechApi = tangemTechApi, appCurrencyResponseStore = appCurrencyResponseStore, dispatchers = dispatchers, diff --git a/data/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/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 626219cb64..bdb663a190 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.staking.store.StakeKitBalancesStore import com.tangem.domain.card.common.TapWorkarounds.isWallet2 import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWallet @@ -156,6 +157,10 @@ internal class DefaultStakingRepository( return false } + if (userWallet.scanResponse.productType == ProductType.Note) { + return true + } + val blockchainId = cryptoCurrency.network.rawId return when { isSolana(blockchainId) -> INVALID_BATCHES_FOR_SOLANA.contains(userWallet.scanResponse.card.batchId) diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt index 4a1a5c9a1b..c957ac3d12 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcher.kt @@ -17,7 +17,8 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrap import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.datasource.local.token.P2PEthPoolVaultsStore import com.tangem.datasource.local.token.StakingYieldsStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncOrNull import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWallet @@ -41,20 +42,20 @@ import javax.inject.Inject * * Supports both StakeKit and P2PEthPool staking providers. * - * @property userWalletsStore user wallets store - * @property stakingYieldsStore staking yields store - * @property stakeKitBalancesStore staking balances store (StakeKit) - * @property p2PEthPoolBalancesStore P2PEthPool balances store - * @property stakeKitApi stake kit API - * @property p2pEthPoolApi P2PEthPool API - * @property p2pEthPoolVaultsStore P2PEthPool vaults store - * @property dispatchers dispatchers + * @property userWalletsListRepository repository of user wallets + * @property stakingYieldsStore staking yields store + * @property stakeKitBalancesStore staking balances store (StakeKit) + * @property p2PEthPoolBalancesStore P2PEthPool balances store + * @property stakeKitApi stake kit API + * @property p2pEthPoolApi P2PEthPool API + * @property p2pEthPoolVaultsStore P2PEthPool vaults store + * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ @Suppress("LongParameterList") internal class DefaultMultiStakingBalanceFetcher @Inject constructor( - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val stakingYieldsStore: StakingYieldsStore, private val stakeKitBalancesStore: StakeKitBalancesStore, private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore, @@ -238,7 +239,7 @@ internal class DefaultMultiStakingBalanceFetcher @Inject constructor( } private inline fun checkIsSupportedByWalletOrElse(userWalletId: UserWalletId, ifNotSupported: (Throwable) -> Unit) { - val maybeUserWallet = userWalletsStore.getSyncOrNull(key = userWalletId).toOption() + val maybeUserWallet = userWalletsListRepository.getSyncOrNull(id = userWalletId).toOption() val isSupportedByWallet = maybeUserWallet.isSome(UserWallet::isMultiCurrency) diff --git a/data/staking/src/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/multi/DefaultMultiStakingBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt index 65412ad690..3290286794 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiStakingBalanceFetcherTest.kt @@ -14,14 +14,14 @@ import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.token.P2PEthPoolVaultsStore import com.tangem.datasource.local.token.StakingYieldsStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher import com.tangem.test.core.assertEitherLeft import com.tangem.test.core.assertEitherRight import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -33,7 +33,7 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DefaultMultiStakingBalanceFetcherTest { - private val userWalletsStore: UserWalletsStore = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk() private val stakingYieldsStore: StakingYieldsStore = mockk() private val stakeKitBalancesStore: StakeKitBalancesStore = mockk(relaxUnitFun = true) private val p2PEthPoolBalancesStore: P2PEthPoolBalancesStore = mockk(relaxUnitFun = true) @@ -42,7 +42,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { private val p2pEthPoolVaultsStore: P2PEthPoolVaultsStore = mockk() private val fetcher = DefaultMultiStakingBalanceFetcher( - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, stakingYieldsStore = stakingYieldsStore, stakeKitBalancesStore = stakeKitBalancesStore, p2PEthPoolBalancesStore = p2PEthPoolBalancesStore, @@ -54,7 +54,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { @BeforeEach fun resetMocks() { - clearMocks(userWalletsStore, stakingYieldsStore, stakeKitBalancesStore, stakeKitApi) + clearMocks(userWalletsListRepository, stakingYieldsStore, stakeKitBalancesStore, stakeKitApi) } @Test @@ -62,7 +62,9 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId)) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields @@ -80,7 +82,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getMultipleYieldBalances(requests) @@ -97,7 +99,9 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val yields = listOf(MockYieldDTOFactory.create(tonId)) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields @@ -112,7 +116,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(solanaId)) @@ -129,13 +133,15 @@ internal class DefaultMultiStakingBalanceFetcherTest { val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow // Actual val actual = fetcher.invoke(params) // Assert - coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) } + coVerifyOrder { userWalletsListRepository.userWallets } coVerify(inverse = true) { stakeKitBalancesStore.refresh(userWalletId = any(), stakingIds = any()) @@ -155,13 +161,14 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns null + val userWalletsFlow = MutableStateFlow(null) + coEvery { userWalletsListRepository.userWallets } returns userWalletsFlow // Actual val actual = fetcher.invoke(params) // Assert - coVerifyOrder { userWalletsStore.getSyncOrNull(params.userWalletId) } + coVerifyOrder { userWalletsListRepository.userWallets } coVerify(inverse = true) { stakeKitBalancesStore.refresh(userWalletId = any(), stakingIds = any()) @@ -181,7 +188,9 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { stakingYieldsStore.getSyncWithTimeout() } returns null // Actual @@ -189,7 +198,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets stakeKitBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -210,7 +219,9 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { stakingYieldsStore.getSyncWithTimeout() } returns emptyList() // Actual @@ -218,7 +229,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -239,7 +250,9 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val yields = listOf( MockYieldDTOFactory.create(tonId).copy(id = null), @@ -252,7 +265,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -273,7 +286,9 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val yields = listOf(MockYieldDTOFactory.create(StakingID(integrationId = "polygon", address = "0x1"))) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields @@ -283,7 +298,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitBalancesStore.storeError(userWalletId, tonAndSolanaIds) @@ -310,7 +325,9 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Arrange val params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) - coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + val userWalletsFlow = MutableStateFlow(listOf(userWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId)) coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields @@ -328,7 +345,7 @@ internal class DefaultMultiStakingBalanceFetcherTest { // Assert coVerifyOrder { - userWalletsStore.getSyncOrNull(params.userWalletId) + userWalletsListRepository.userWallets stakeKitBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) stakingYieldsStore.getSyncWithTimeout() stakeKitApi.getMultipleYieldBalances(requests) @@ -343,8 +360,8 @@ internal class DefaultMultiStakingBalanceFetcherTest { } private companion object { - val userWalletId = UserWalletId("011") val userWallet = MockUserWalletFactory.create() + val userWalletId = userWallet.walletId val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId val solanaId = StakingID( diff --git a/data/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/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index d95f8ab901..0386fa396c 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -4,6 +4,8 @@ import arrow.core.none import arrow.core.some import arrow.core.toOption import com.squareup.moshi.Moshi +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.api.safeApiCall import com.tangem.data.swap.converter.SwapDataConverter import com.tangem.data.swap.converter.SwapStatusConverter @@ -94,9 +96,10 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( currencyStatus.currency.network.backendId == pair.to.network } - val mappedProviders = pair.providers.mapNotNull { - expressProviders[it.providerId] - }.filterYieldSupplyProvider(statusFrom) + val mappedProviders = pair.providers + .filterNot { it.hasOnlyFixedRateType() } + .mapNotNull { expressProviders[it.providerId] } + .filterYieldSupplyProvider(statusFrom) if (statusFrom != null && statusTo != null && mappedProviders.isNotEmpty()) { SwapPairModel( @@ -131,41 +134,43 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( swapTxType = swapTxType, ).associateBy(ExpressProvider::providerId) - allPairs.map { pair -> - async { - val statusFromDeferred = async { - cryptoCurrencyList - .firstOrNull { currency -> - currency.getContractAddress() == pair.from.contractAddress && - currency.network.backendId == pair.from.network - } - } - val statusToDeferred = async { - cryptoCurrencyList - .firstOrNull { currency -> - currency.getContractAddress() == pair.to.contractAddress && - currency.network.backendId == pair.to.network - } - } + allPairs.filter { it.to.network !in MEMO_RESTRICTED_NETWORKS } + .map { pair -> + async { + val statusFromDeferred = async { + cryptoCurrencyList + .firstOrNull { currency -> + currency.getContractAddress() == pair.from.contractAddress && + currency.network.backendId == pair.from.network + } + } + val statusToDeferred = async { + cryptoCurrencyList + .firstOrNull { currency -> + currency.getContractAddress() == pair.to.contractAddress && + currency.network.backendId == pair.to.network + } + } - val currencyStatusFrom = createSendWithSwapCryptoCurrencyStatus(statusFromDeferred.await()) - val currencyStatusTo = createSendWithSwapCryptoCurrencyStatus(statusToDeferred.await()) + val currencyStatusFrom = createSendWithSwapCryptoCurrencyStatus(statusFromDeferred.await()) + val currencyStatusTo = createSendWithSwapCryptoCurrencyStatus(statusToDeferred.await()) - val mappedProvider = pair.providers.mapNotNull { - mappedProviders[it.providerId] - }.filterYieldSupplyProvider(currencyStatusFrom) + val mappedProvider = pair.providers + .filterNot { it.hasOnlyFixedRateType() } + .mapNotNull { mappedProviders[it.providerId] } + .filterYieldSupplyProvider(currencyStatusFrom) - if (currencyStatusFrom != null && currencyStatusTo != null && mappedProvider.isNotEmpty()) { - SwapPairModel( - from = currencyStatusFrom, - to = currencyStatusTo, - providers = mappedProvider, - ) - } else { - null + if (currencyStatusFrom != null && currencyStatusTo != null && mappedProvider.isNotEmpty()) { + SwapPairModel( + from = currencyStatusFrom, + to = currencyStatusTo, + providers = mappedProvider, + ) + } else { + null + } } - } - }.awaitAll().filterNotNull() + }.awaitAll().filterNotNull() } override suspend fun getSwapQuote( @@ -208,6 +213,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( toCryptoCurrency: CryptoCurrency, fromAmount: String, toAddress: String, + toExtraId: String?, expressProvider: ExpressProvider, rateType: ExpressRateType, expressOperationType: ExpressOperationType, @@ -247,6 +253,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( userWallet = userWallet, appPreferencesStore = appPreferencesStore, ), + toExtraId = toExtraId?.ifEmpty { null }, ).getOrThrow() if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) { @@ -436,6 +443,14 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( } } +private val MEMO_RESTRICTED_NETWORKS = setOf( + Blockchain.XRP.toNetworkId(), + Blockchain.Stellar.toNetworkId(), + Blockchain.InternetComputer.toNetworkId(), + Blockchain.Casper.toNetworkId(), + Blockchain.Algorand.toNetworkId(), +) + private suspend fun ExpressRepository.getFilteredProviders( userWallet: UserWallet, filterProviderTypes: List, diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt index cbcb56f715..3734069fb2 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt @@ -60,8 +60,8 @@ internal class DefaultSwapTransactionRepository( userWalletId: UserWalletId, fromCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, transaction: SwapTransactionModel, ) { transaction.status?.let { swapTxList -> @@ -244,8 +244,8 @@ internal class DefaultSwapTransactionRepository( userWalletId: UserWalletId, fromCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, transactions: List, ): List { return addOrReplace( diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt index 552341da60..aa9d27b4b7 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/transaction/SavedSwapTransactionListConverter.kt @@ -101,8 +101,8 @@ internal class SavedSwapTransactionListConverter( userWalletId: UserWalletId, fromCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, tokenTransactions: List, ) = SwapTransactionListDTO( userWalletId = userWalletId.stringValue, diff --git a/data/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/detekt-baseline-debug.xml b/data/tokens/detekt-baseline-debug.xml deleted file mode 100644 index bc443d0652..0000000000 --- a/data/tokens/detekt-baseline-debug.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - MultilineLambdaItParameter:CustomTokensMerger.kt$CustomTokensMerger${ Timber.e(it, "Unable to fetch token:\n$token") null } - NullableToStringCall:AccountListCryptoCurrenciesFetcher.kt$AccountListCryptoCurrenciesFetcher$${this::class.simpleName} - NullableToStringCall:DefaultMultiWalletCryptoCurrenciesFetcher.kt$DefaultMultiWalletCryptoCurrenciesFetcher$${this::class.simpleName} - UseOrEmpty:DefaultYieldSupplyWarningsViewedRepository.kt$DefaultYieldSupplyWarningsViewedRepository$appPreferencesStore.getObjectSet<String>(PreferencesKeys.YIELD_SUPPLY_WARNINGS_STATES_KEY).firstOrNull() ?: emptySet() - - diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcher.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcher.kt index bf34ca207d..642add41ce 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcher.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcher.kt @@ -4,11 +4,11 @@ import arrow.core.Either import arrow.core.right import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.datasource.api.tangemTech.models.account.flattenTokens -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.core.utils.catchOn import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.express.models.ExpressAsset -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher.Params @@ -17,7 +17,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider /** * Implementation of [MultiWalletCryptoCurrenciesFetcher] that fetches crypto currencies of all accounts * - * @property userWalletsStore [UserWallet]'s store + * @property userWalletsListRepository repository to get user wallets * @property walletAccountsFetcher instance of [WalletAccountsFetcher] to fetch accounts for a multi wallet * @property expressServiceFetcher fetcher of express service * @property dispatchers dispatchers @@ -25,7 +25,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider [REDACTED_AUTHOR] */ internal class AccountListCryptoCurrenciesFetcher( - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val walletAccountsFetcher: WalletAccountsFetcher, private val expressServiceFetcher: ExpressServiceFetcher, private val dispatchers: CoroutineDispatcherProvider, @@ -33,7 +33,7 @@ internal class AccountListCryptoCurrenciesFetcher( override suspend fun invoke(params: Params): Either { return Either.catchOn(dispatchers.default) { - val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet") diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt index 8fc010aa4d..4b563eb468 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt @@ -10,7 +10,8 @@ import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.core.utils.catchOn import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.express.ExpressServiceFetcher @@ -26,7 +27,6 @@ import timber.log.Timber /** * Default implementation of [MultiWalletCryptoCurrenciesFetcher] * - * @property userWalletsStore [UserWallet]'s store * @property tangemTechApi Tangem Tech API * @property userTokensResponseStore store of [UserTokensResponse] * @property userTokensSaver user tokens saver @@ -39,7 +39,7 @@ import timber.log.Timber @Suppress("LongParameterList") internal class DefaultMultiWalletCryptoCurrenciesFetcher( private val demoConfig: DemoConfig, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val tangemTechApi: TangemTechApi, private val customTokensMerger: CustomTokensMerger, private val userTokensResponseStore: UserTokensResponseStore, @@ -52,7 +52,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcher( private val userTokensResponseFactory = UserTokensResponseFactory() override suspend fun invoke(params: Params) = Either.catchOn(dispatchers.default) { - val userWallet = userWalletsStore.getSyncStrict(key = params.userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet") diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt index 8d06bf0af1..e3e6f70446 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt @@ -8,8 +8,8 @@ import com.tangem.data.tokens.DefaultMultiWalletCryptoCurrenciesFetcher import com.tangem.data.tokens.utils.CustomTokensMerger import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher @@ -29,7 +29,7 @@ internal class MultiWalletCryptoCurrenciesFetcherModule { fun provideMultiWalletCryptoCurrenciesFetcher( accountsFeatureToggles: AccountsFeatureToggles, tangemTechApi: TangemTechApi, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, userTokensResponseStore: UserTokensResponseStore, userTokensSaver: UserTokensSaver, cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, @@ -39,7 +39,7 @@ internal class MultiWalletCryptoCurrenciesFetcherModule { ): MultiWalletCryptoCurrenciesFetcher { return if (accountsFeatureToggles.isFeatureEnabled) { AccountListCryptoCurrenciesFetcher( - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, walletAccountsFetcher = walletAccountsFetcher, expressServiceFetcher = expressServiceFetcher, dispatchers = dispatchers, @@ -47,7 +47,7 @@ internal class MultiWalletCryptoCurrenciesFetcherModule { } else { DefaultMultiWalletCryptoCurrenciesFetcher( demoConfig = DemoConfig, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, tangemTechApi = tangemTechApi, customTokensMerger = CustomTokensMerger( tangemTechApi = tangemTechApi, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 8c0efab318..f30629e0d4 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -13,8 +13,8 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.TokenReceiveWarningActionStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -39,7 +39,7 @@ internal object TokensDataModule { fun provideCurrenciesRepository( tangemTechApi: TangemTechApi, userTokensResponseStore: UserTokensResponseStore, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, walletManagersFacade: WalletManagersFacade, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, @@ -53,7 +53,7 @@ internal object TokensDataModule { ): CurrenciesRepository { return DefaultCurrenciesRepository( tangemTechApi = tangemTechApi, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, walletManagersFacade = walletManagersFacade, cacheRegistry = cacheRegistry, userTokensResponseStore = userTokensResponseStore, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index ffb98afb6c..b2ec4a4968 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -13,10 +13,12 @@ import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict +import com.tangem.domain.common.wallets.loadAndGet import com.tangem.domain.core.error.DataError import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.express.ExpressServiceFetcher @@ -41,7 +43,7 @@ import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") internal class DefaultCurrenciesRepository( private val tangemTechApi: TangemTechApi, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val walletManagersFacade: WalletManagersFacade, private val cacheRegistry: CacheRegistry, private val expressServiceFetcher: ExpressServiceFetcher, @@ -104,7 +106,7 @@ internal class DefaultCurrenciesRepository( ) fetchExpressAssetsByNetworkIds( - userWallet = userWalletsStore.getSyncStrict(key = userWalletId), + userWallet = userWalletsListRepository.getSyncStrict(id = userWalletId), userTokens = updatedResponse, ) @@ -194,7 +196,7 @@ internal class DefaultCurrenciesRepository( override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { return channelFlow { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) if (userWallet.isMultiCurrency) { getMultiCurrencyWalletCurrenciesUpdates(userWalletId).collect(::send) @@ -211,7 +213,7 @@ internal class DefaultCurrenciesRepository( refresh: Boolean, ): CryptoCurrency { return withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) userWallet.requireColdWallet() ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) @@ -234,7 +236,7 @@ internal class DefaultCurrenciesRepository( refresh: Boolean, ): List { return withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) val scanResponse = userWallet.requireColdWallet().scanResponse val currencies = if (scanResponse.cardTypesResolver.isSingleWalletWithToken()) { @@ -261,7 +263,7 @@ internal class DefaultCurrenciesRepository( id: CryptoCurrency.ID, ): CryptoCurrency { return withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) userWallet.requireColdWallet() ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) @@ -277,7 +279,7 @@ internal class DefaultCurrenciesRepository( private fun getMultiCurrencyWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { return channelFlow { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) getMultiCurrencyWalletCurrencies(userWallet) @@ -290,7 +292,7 @@ internal class DefaultCurrenciesRepository( userWalletId: UserWalletId, refresh: Boolean, ): List = withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) fetchTokensIfCacheExpired(userWallet, refresh) @@ -344,7 +346,7 @@ internal class DefaultCurrenciesRepository( derivationPath: Network.DerivationPath, ): CryptoCurrency.Coin { return withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true) fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false) @@ -378,7 +380,7 @@ internal class DefaultCurrenciesRepository( override fun isTokensGrouped(userWalletId: UserWalletId): Flow { return channelFlow { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) if (userWallet.isMultiCurrency) { getSavedUserTokensResponse(userWalletId) @@ -394,7 +396,7 @@ internal class DefaultCurrenciesRepository( override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { return channelFlow { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) if (userWallet.isMultiCurrency) { getSavedUserTokensResponse(userWalletId) @@ -481,7 +483,7 @@ internal class DefaultCurrenciesRepository( contractAddress: String, networkId: String, ): CryptoCurrency.Token { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) val token = withContext(dispatchers.io) { val foundToken = tangemTechApi.getCoins( contractAddress = contractAddress, @@ -513,7 +515,7 @@ internal class DefaultCurrenciesRepository( override fun getAllWalletsCryptoCurrencies( currencyRawId: CryptoCurrency.RawID, ): Flow>> { - return userWalletsStore.userWallets.flatMapLatest { userWallets -> + return userWalletsListRepository.loadAndGet().flatMapLatest { userWallets -> userWallets.filter { it.isMultiCurrency } .forEach { fetchTokensIfCacheExpired(userWallet = it, refresh = false) } @@ -589,7 +591,7 @@ internal class DefaultCurrenciesRepository( } override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver? { - return (userWalletsStore.getSyncStrict(userWalletId) as? UserWallet.Cold)?.cardTypesResolver + return (userWalletsListRepository.getSyncStrict(userWalletId) as? UserWallet.Cold)?.cardTypesResolver } private fun getMultiCurrencyWalletCurrencies(userWallet: UserWallet): Flow> { @@ -711,7 +713,7 @@ internal class DefaultCurrenciesRepository( ) private fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/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/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultYieldSupplyWarningsViewedRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultYieldSupplyWarningsViewedRepository.kt index 193bbb03c6..c9662d98f7 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultYieldSupplyWarningsViewedRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultYieldSupplyWarningsViewedRepository.kt @@ -14,8 +14,8 @@ internal class DefaultYieldSupplyWarningsViewedRepository( ) : YieldSupplyWarningsViewedRepository { override suspend fun getViewedWarnings(): Set = withContext(dispatchers.io) { - appPreferencesStore.getObjectSet(PreferencesKeys.YIELD_SUPPLY_WARNINGS_STATES_KEY).firstOrNull() - ?: emptySet() + appPreferencesStore.getObjectSet(PreferencesKeys.YIELD_SUPPLY_WARNINGS_STATES_KEY) + .firstOrNull().orEmpty() } override suspend fun view(symbol: String) = withContext(dispatchers.io) { diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CustomTokensMerger.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CustomTokensMerger.kt index 2467b477da..4d0cf9c1fc 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CustomTokensMerger.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/CustomTokensMerger.kt @@ -104,8 +104,8 @@ internal class CustomTokensMerger( networkIds = token.networkId, ).bind() }, - onError = { - Timber.e(it, "Unable to fetch token:\n$token") + onError = { error -> + Timber.e(error, "Unable to fetch token:\n$token") null }, ) diff --git a/data/tokens/src/test/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcherTest.kt b/data/tokens/src/test/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcherTest.kt index 7535e139d9..3f95f270ca 100644 --- a/data/tokens/src/test/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcherTest.kt +++ b/data/tokens/src/test/kotlin/com/tangem/data/tokens/AccountListCryptoCurrenciesFetcherTest.kt @@ -4,7 +4,7 @@ import arrow.core.left import arrow.core.right import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -14,6 +14,7 @@ import com.tangem.test.core.assertEither import com.tangem.test.core.assertEitherRight import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -22,13 +23,13 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class AccountListCryptoCurrenciesFetcherTest { - private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true) + private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) private val walletAccountsFetcher: WalletAccountsFetcher = mockk(relaxUnitFun = true) private val expressServiceFetcher: ExpressServiceFetcher = mockk() private val dispatchers = TestingCoroutineDispatcherProvider() private val fetcher = AccountListCryptoCurrenciesFetcher( - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, walletAccountsFetcher = walletAccountsFetcher, expressServiceFetcher = expressServiceFetcher, dispatchers = dispatchers, @@ -36,15 +37,20 @@ internal class AccountListCryptoCurrenciesFetcherTest { @BeforeEach fun resetMocks() { - clearMocks(userWalletsStore, walletAccountsFetcher) + clearMocks(userWalletsListRepository, walletAccountsFetcher) } @Test fun `returns failure if wallet is not multi-currency`() = runTest { // Arrange val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - val mockUserWallet = mockk { every { isMultiCurrency } returns false } - every { userWalletsStore.getSyncStrict(key = params.userWalletId) } returns mockUserWallet + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns false + } + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow // Act val actual = fetcher(params) @@ -55,7 +61,7 @@ internal class AccountListCryptoCurrenciesFetcherTest { ).left() assertEither(actual, expected) - verify { userWalletsStore.getSyncStrict(key = params.userWalletId) } + verify { userWalletsListRepository.userWallets } coVerify(inverse = true) { walletAccountsFetcher.fetch(any()) } } @@ -63,10 +69,15 @@ internal class AccountListCryptoCurrenciesFetcherTest { fun `returns accounts if wallet is multi-currency`() = runTest { // Arrange val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - val mockUserWallet = mockk { every { isMultiCurrency } returns true } + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + } val response = mockk(relaxed = true) - every { userWalletsStore.getSyncStrict(key = params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { walletAccountsFetcher.fetch(userWalletId = params.userWalletId) } returns response coEvery { expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = emptySet()) } returns Unit.right() @@ -77,7 +88,7 @@ internal class AccountListCryptoCurrenciesFetcherTest { assertEitherRight(actual) coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.userWallets walletAccountsFetcher.fetch(userWalletId = params.userWalletId) expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = emptySet()) } @@ -87,10 +98,15 @@ internal class AccountListCryptoCurrenciesFetcherTest { fun `returns error if walletAccountsFetcher returns error`() = runTest { // Arrange val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - val mockUserWallet = mockk { every { isMultiCurrency } returns true } + val mockUserWallet = mockk { + every { walletId } returns userWalletId + every { isMultiCurrency } returns true + } val error = RuntimeException("fetch error") - every { userWalletsStore.getSyncStrict(key = params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { walletAccountsFetcher.fetch(userWalletId = params.userWalletId) } throws error // Act @@ -101,7 +117,7 @@ internal class AccountListCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.userWallets walletAccountsFetcher.fetch(userWalletId = params.userWalletId) } } diff --git a/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt index 991d4f40f5..1b649ebbe0 100644 --- a/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt +++ b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt @@ -13,7 +13,7 @@ import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.express.models.ExpressAsset @@ -24,6 +24,7 @@ import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher import com.tangem.test.core.assertEither import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -38,7 +39,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() private val userTokensResponseFactory = UserTokensResponseFactory() - private val userWalletsStore: UserWalletsStore = mockk(relaxUnitFun = true) + private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) private val tangemTechApi: TangemTechApi = mockk() private val customTokensMerger: CustomTokensMerger = mockk() private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true) @@ -48,7 +49,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { private val fetcher = DefaultMultiWalletCryptoCurrenciesFetcher( demoConfig = DemoConfig, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, tangemTechApi = tangemTechApi, customTokensMerger = customTokensMerger, userTokensResponseStore = userTokensResponseStore, @@ -61,7 +62,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { @BeforeEach fun resetMocks() { clearMocks( - userWalletsStore, + userWalletsListRepository, tangemTechApi, userTokensResponseStore, userTokensSaver, @@ -76,10 +77,13 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) val mockUserWallet = mockk { + every { walletId } returns userWalletId every { isMultiCurrency } returns false } - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow // Act val actual = fetcher(params) @@ -90,7 +94,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { ).left() assertEither(actual, expected) - verifyOrder { userWalletsStore.getSyncStrict(key = params.userWalletId) } + verifyOrder { userWalletsListRepository.userWallets } coVerify(inverse = true) { userTokensResponseStore.getSyncOrNull(any()) } @@ -121,7 +125,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { ), ) - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns null every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) @@ -145,7 +151,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.userWallets userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) @@ -169,7 +175,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN), ) - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns defaultResponse coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { @@ -191,7 +199,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.userWallets userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) @@ -219,7 +227,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN), ) - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) @@ -240,7 +250,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.userWallets tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data) @@ -283,7 +293,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { ), ) - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null coEvery { @@ -308,7 +320,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.userWallets tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) @@ -337,7 +349,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { cause = ApiResponseError.TimeoutException(), ) as ApiResponse - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns defaultResponse coEvery { @@ -359,7 +373,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.userWallets tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) @@ -407,7 +421,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { ), ) - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null coEvery { @@ -432,7 +448,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.userWallets tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) @@ -463,7 +479,9 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { ), ) as ApiResponse - every { userWalletsStore.getSyncStrict(params.userWalletId) } returns mockUserWallet + val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns defaultResponse coEvery { @@ -485,7 +503,7 @@ internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { assertEither(actual, expected) coVerifyOrder { - userWalletsStore.getSyncStrict(key = params.userWalletId) + userWalletsListRepository.userWallets tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) userTokensSaver.push(userWalletId = params.userWalletId, response = defaultResponse) diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index f7c4ffff50..06b461cbd4 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -422,7 +422,11 @@ internal class DefaultTransactionRepository( preparer.prepareAndSignMultiple(transactionData, signer) } - override suspend fun sendTransactionHash(hash: String, transactionType: EventTransactionTypeDto) { + override suspend fun sendTransactionHash( + hash: String, + transactionType: EventTransactionTypeDto, + userAddress: String?, + ) { runCatching(dispatchers.io) { val operationType = when (transactionType) { EventTransactionTypeDto.DEPOSIT -> OperationType.YIELD_DEPOSIT @@ -432,6 +436,7 @@ internal class DefaultTransactionRepository( val body = TransactionEventBody( operationType = operationType, transactionId = hash, + userAddress = userAddress, ) val response = tangemTechApi.transactionEvents(body) response.fold( diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt index 49244f2da1..5c2bbe6654 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultWalletAddressServiceRepository.kt @@ -1,12 +1,14 @@ package com.tangem.data.transaction import android.net.Uri -import androidx.core.text.isDigitsOnly import com.tangem.blockchain.blockchains.near.NearWalletManager import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.NameResolver import com.tangem.blockchain.common.ResolveAddressResult import com.tangem.blockchain.common.ReverseResolveAddressResult +import com.tangem.blockchain.common.TransactionValidator +import com.tangem.blockchain.common.memo.MemoState +import com.tangem.blockchain.extensions.Result import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId @@ -16,7 +18,6 @@ import com.tangem.domain.wallets.models.ParsedQrCode import com.tangem.domain.wallets.models.errors.ParsedQrCodeErrors import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext -import java.math.BigInteger class DefaultWalletAddressServiceRepository( private val walletManagersFacade: WalletManagersFacade, @@ -95,19 +96,28 @@ class DefaultWalletAddressServiceRepository( } } - override fun validateMemo(network: Network, memo: String): Boolean { - if (memo.isEmpty()) return true - return when (network.rawId) { - Blockchain.XRP.id -> { - val tag = memo.toLongOrNull() - tag != null && tag <= XRP_TAG_MAX_NUMBER + override suspend fun validateMemo(userWalletId: UserWalletId, network: Network, memo: String): Boolean = + withContext(dispatchers.io) { + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + network = network, + ) ?: return@withContext true + + val memoStateResult = (walletManager as? TransactionValidator)?.validateMemo(memo) + if (memoStateResult != null) { + when (memoStateResult) { + is Result.Success -> when (memoStateResult.data) { + MemoState.NotSupported, + MemoState.Valid, + -> true + MemoState.Invalid -> false + } + is Result.Failure -> true + } + } else { + true } - Blockchain.Stellar.id -> { - isAssignableXlmValue(memo) - } - else -> true } - } override suspend fun parseSharedAddress(input: String, network: Network): ParsedQrCode { val blockchain = network.toBlockchain() @@ -145,26 +155,4 @@ class DefaultWalletAddressServiceRepository( private fun Blockchain.isNear(): Boolean { return this == Blockchain.Near || this == Blockchain.NearTestnet } - - private fun isAssignableXlmValue(value: String): Boolean { - return when { - value.isNotEmpty() && value.isDigitsOnly() -> { - try { - // from com.tangem.blockchain.blockchains.stellar.StellarMemo.toStellarSdkMemo - value.toBigInteger() in BigInteger.ZERO..Long.MAX_VALUE.toBigInteger() * 2.toBigInteger() - } catch (ex: NumberFormatException) { - false - } - } - else -> { - // from org.stellar.sdk.MemoText - value.toByteArray().size <= XLM_MEMO_MAX_LENGTH - } - } - } - - companion object { - private const val XLM_MEMO_MAX_LENGTH = 28 - private const val XRP_TAG_MAX_NUMBER = 4294967295 - } } \ No newline at end of file diff --git a/data/txhistory/build.gradle.kts b/data/txhistory/build.gradle.kts index d7bceabb15..78b3545254 100644 --- a/data/txhistory/build.gradle.kts +++ b/data/txhistory/build.gradle.kts @@ -15,15 +15,18 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.datasource) implementation(projects.core.pagination) + implementation(projects.domain.legacy) + implementation(projects.domain.common) implementation(projects.domain.walletManager) - implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory) implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) + implementation(projects.libs.blockchainSdk) + implementation(deps.kotlin.coroutines) implementation(deps.androidx.paging.runtime) implementation(deps.timber) diff --git a/data/txhistory/detekt-baseline-debug.xml b/data/txhistory/detekt-baseline-debug.xml index 3dfda1c580..02aca3d1f9 100644 --- a/data/txhistory/detekt-baseline-debug.xml +++ b/data/txhistory/detekt-baseline-debug.xml @@ -3,11 +3,7 @@ BooleanPropertyNaming:TxHistoryPagingSource.kt$TxHistoryPagingSource.Params$val refresh: Boolean - MaxChainedCallsOnSameLine:DefaultTxHistoryRepository.kt$DefaultTxHistoryRepository$walletManager?.wallet?.recentTransactions?.last()?.hash.orEmpty() MultilineLambdaItParameter:TxHistoryPageBatchFetcher.kt$TxHistoryPageBatchFetcher${ currentCoroutineContext().ensureActive() BatchFetchResult.Error(it) } - NamedArguments:DefaultTxHistoryRepository.kt$DefaultTxHistoryRepository$Params(userWalletId, currency, pageSize, refresh) - NamedArguments:TxHistoryDataModule.kt$TxHistoryDataModule$DefaultTxHistoryRepository( cacheRegistry, walletManagersFacade, userWalletsStore, txHistoryItemsStore, dispatchers, ) SuspendFunSwallowedCancellation:TxHistoryPageBatchFetcher.kt$TxHistoryPageBatchFetcher$runCatching - UseOrEmpty:DefaultTxHistoryRepository.kt$DefaultTxHistoryRepository$txs ?: emptyList() diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt index 1a756f54e1..7eab581f07 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/di/TxHistoryDataModule.kt @@ -4,7 +4,7 @@ import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.txhistory.repository.DefaultTxHistoryRepository import com.tangem.data.txhistory.repository.RefactoredTxHistoryRepository import com.tangem.datasource.local.txhistory.TxHistoryItemsStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.txhistory.repository.TxHistoryRepository import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2 import com.tangem.domain.walletmanager.WalletManagersFacade @@ -24,15 +24,15 @@ internal object TxHistoryDataModule { fun provideTxHistoryRepository( cacheRegistry: CacheRegistry, walletManagersFacade: WalletManagersFacade, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, txHistoryItemsStore: TxHistoryItemsStore, dispatchers: CoroutineDispatcherProvider, ): TxHistoryRepository = DefaultTxHistoryRepository( - cacheRegistry, - walletManagersFacade, - userWalletsStore, - txHistoryItemsStore, - dispatchers, + cacheRegistry = cacheRegistry, + walletManagersFacade = walletManagersFacade, + userWalletsListRepository = userWalletsListRepository, + txHistoryItemsStore = txHistoryItemsStore, + dispatchers = dispatchers, ) @Provides diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt index b668562534..23761f8743 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/DefaultTxHistoryRepository.kt @@ -8,10 +8,10 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.txhistory.repository.paging.TxHistoryPagingSource import com.tangem.datasource.local.txhistory.TxHistoryItemsStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.TxInfo -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.txhistory.models.Page import com.tangem.domain.txhistory.models.TxHistoryState @@ -27,7 +27,7 @@ import timber.log.Timber class DefaultTxHistoryRepository( private val cacheRegistry: CacheRegistry, private val walletManagersFacade: WalletManagersFacade, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val txHistoryItemsStore: TxHistoryItemsStore, private val dispatchers: CoroutineDispatcherProvider, ) : TxHistoryRepository { @@ -35,7 +35,7 @@ class DefaultTxHistoryRepository( override suspend fun getTxHistoryItemsCount(userWalletId: UserWalletId, currency: CryptoCurrency): Int { return withContext(dispatchers.io) { - val userWallet = getUserWallet(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) val state = walletManagersFacade.getTxHistoryState( userWalletId = userWallet.walletId, currency = currency, @@ -63,7 +63,12 @@ class DefaultTxHistoryRepository( ), pagingSourceFactory = { TxHistoryPagingSource( - sourceParams = TxHistoryPagingSource.Params(userWalletId, currency, pageSize, refresh), + sourceParams = TxHistoryPagingSource.Params( + userWalletId = userWalletId, + currency = currency, + pageSize = pageSize, + refresh = refresh, + ), txHistoryItemsStore = txHistoryItemsStore, walletManagersFacade = walletManagersFacade, cacheRegistry = cacheRegistry, @@ -98,11 +103,12 @@ class DefaultTxHistoryRepository( skipCache = shouldRefresh, block = { fetchFixedSizeTxHistoryItems(userWalletId, currency, pageSize) }, ) - val txs = txHistoryItemsStore.getSyncOrNull( + + txHistoryItemsStore.getSyncOrNull( key = TxHistoryItemsStore.Key(userWalletId, currency), page = Page.Initial, - )?.items - txs ?: emptyList() + ) + ?.items.orEmpty() } catch (e: Throwable) { Timber.e(e, "Unable to load the transaction history for the requested page: ${Page.Initial}") emptyList() @@ -127,10 +133,4 @@ class DefaultTxHistoryRepository( txHistoryItemsStore.store(TxHistoryItemsStore.Key(userWalletId, currency), wrappedItems) } - - private fun getUserWallet(userWalletId: UserWalletId): UserWallet { - return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "Unable to find user wallet with provided ID: $userWalletId" - } - } } \ No newline at end of file diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 3c9ef9ff07..67768d9958 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(projects.domain.walletManager) implementation(projects.domain.quotes) implementation(projects.domain.common) + implementation(projects.features.swap.domain) /** Feature API - remove after removing [HotWalletFeatureToggles] */ implementation(projects.features.hotWallet.api) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/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/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index 500e5fa50d..5868e4c0c6 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -45,7 +45,7 @@ internal interface TangemPayDataModule { @Binds @Singleton - fun bindTangemPaySwapRepository(repository: DefaultTangemPaySwapRepository): TangemPaySwapRepository + fun bindTangemPaySwapRepository(repository: DefaultTangemPayWithdrawRepository): TangemPayWithdrawRepository @Binds @Singleton diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/entity/WithdrawStoreData.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/entity/WithdrawStoreData.kt new file mode 100644 index 0000000000..02d63db72f --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/entity/WithdrawStoreData.kt @@ -0,0 +1,20 @@ +package com.tangem.data.pay.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = false) +data class WithdrawStoreData( + @Json(name = "orderId") val orderId: String, + @Json(name = "exchangeData") val exchangeData: ExchangeStoreData?, + @Json(name = "txHash") val txHash: String?, +) + +@JsonClass(generateAdapter = false) +data class ExchangeStoreData( + @Json(name = "txId") val txId: String, + @Json(name = "fromNetwork") val fromNetwork: String, + @Json(name = "fromAddress") val fromAddress: String, + @Json(name = "payInAddress") val payInAddress: String, + @Json(name = "payInExtraId") val payInExtraId: String?, +) \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt index 11afd970e8..f479794766 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt @@ -2,8 +2,8 @@ package com.tangem.data.pay.repository import arrow.core.Either import com.tangem.datasource.api.pay.TangemPayApi -import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.model.OrderData import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.visa.error.VisaApiError @@ -12,35 +12,23 @@ import javax.inject.Inject internal class DefaultCustomerOrderRepository @Inject constructor( private val tangemPayApi: TangemPayApi, private val requestHelper: TangemPayRequestPerformer, - private val tangemPayStorage: TangemPayStorage, ) : CustomerOrderRepository { - override suspend fun getOrderStatus( - userWalletId: UserWalletId, - orderId: String, - ): Either { + override suspend fun getOrderData(userWalletId: UserWalletId, orderId: String): Either { return requestHelper.performRequest(userWalletId) { authHeader -> tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId) }.map { response -> - when (response.result?.status) { + val status = when (response.result?.status) { null -> OrderStatus.UNKNOWN OrderStatus.NEW.apiName -> OrderStatus.NEW OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED else -> OrderStatus.CANCELED } + OrderData( + status = status, + withdrawTxHash = response.result?.data?.transactionHash?.ifEmpty { null }, + ) } } - - override suspend fun hasWithdrawOrder(userWalletId: UserWalletId): Boolean { - val orderId = tangemPayStorage.getWithdrawOrderId(userWalletId) - if (orderId == null) return false - - val status = getOrderStatus(userWalletId, orderId).getOrNull() - - val hasActiveOrder = status == OrderStatus.NEW || status == OrderStatus.PROCESSING - if (!hasActiveOrder) tangemPayStorage.deleteWithdrawOrder(userWalletId) - - return hasActiveOrder - } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/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/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt deleted file mode 100644 index 6bc1774f28..0000000000 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPaySwapRepository.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.tangem.data.pay.repository - -import arrow.core.Either -import arrow.core.left -import com.tangem.core.error.UniversalError -import com.tangem.data.common.quote.QuotesFetcher -import com.tangem.datasource.api.pay.TangemPayApi -import com.tangem.datasource.api.pay.models.request.WithdrawDataRequest -import com.tangem.datasource.api.pay.models.request.WithdrawRequest -import com.tangem.datasource.local.visa.TangemPayStorage -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.pay.WithdrawalResult -import com.tangem.domain.pay.WithdrawalSignatureResult -import com.tangem.domain.pay.datasource.TangemPayAuthDataSource -import com.tangem.domain.pay.repository.TangemPaySwapRepository -import com.tangem.domain.visa.error.VisaApiError -import com.tangem.utils.extensions.addHexPrefix -import java.math.BigDecimal -import java.math.RoundingMode -import java.util.Currency -import java.util.Locale -import javax.inject.Inject - -@Suppress("LongParameterList") -internal class DefaultTangemPaySwapRepository @Inject constructor( - private val tangemPayApi: TangemPayApi, - private val requestHelper: TangemPayRequestPerformer, - private val authDataSource: TangemPayAuthDataSource, - private val quotesFetcher: QuotesFetcher, - private val tangemPayStorage: TangemPayStorage, -) : TangemPaySwapRepository { - - override suspend fun withdraw( - userWallet: UserWallet, - receiverAddress: String, - cryptoAmount: BigDecimal, - cryptoCurrencyId: CryptoCurrency.RawID, - ): Either { - val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId) - if (amountInCents.isNullOrEmpty()) return Either.Left(VisaApiError.WithdrawalDataError) - return requestHelper.performRequest(userWallet.walletId) { authHeader -> - val request = WithdrawDataRequest(amountInCents = amountInCents, recipientAddress = receiverAddress) - tangemPayApi.getWithdrawData(authHeader = authHeader, body = request) - }.map { data -> - val result = data.result ?: return VisaApiError.WithdrawalDataError.left() - val signatureResult = authDataSource.getWithdrawalSignature( - userWallet = userWallet, - hash = result.hash, - ).getOrNull() - - return when (signatureResult) { - is WithdrawalSignatureResult.Cancelled -> { - Either.Right(WithdrawalResult.Cancelled) - } - is WithdrawalSignatureResult.Success -> { - requestHelper.performRequest(userWallet.walletId) { authHeader -> - val request = WithdrawRequest( - amountInCents = amountInCents, - recipientAddress = receiverAddress, - adminSalt = result.salt, - senderAddress = result.senderAddress, - adminSignature = signatureResult.signature.addHexPrefix(), - ) - tangemPayApi.withdraw(authHeader = authHeader, body = request) - } - .mapLeft { return Either.Left(VisaApiError.WithdrawError) } - .map { response -> - val orderId = response.result?.orderId - if (orderId != null) tangemPayStorage.storeWithdrawOrder(userWallet.walletId, orderId) - WithdrawalResult.Success - } - } - null -> return Either.Left(VisaApiError.SignWithdrawError) - } - } - } - - private suspend fun getAmountInCents(cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID): String? { - val fiatRate = getFiatRate(cryptoCurrencyId) ?: return null - val amountInDollars = cryptoAmount.multiply(fiatRate) - val defaultFractionDigits = Currency.getInstance(Locale.US).defaultFractionDigits - return amountInDollars - .setScale(defaultFractionDigits, RoundingMode.HALF_UP) - .movePointRight(defaultFractionDigits) - .longValueExact() - .toString() - } - - private suspend fun getFiatRate(cryptoCurrencyId: CryptoCurrency.RawID): BigDecimal? { - val quotes = quotesFetcher.fetch( - fiatCurrencyId = Currency.getInstance(Locale.US).currencyCode, - currencyId = cryptoCurrencyId.value, - field = QuotesFetcher.Field.PRICE, - ).getOrNull() - - return quotes?.quotes[cryptoCurrencyId.value]?.price - } -} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt new file mode 100644 index 0000000000..7bf21432a7 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt @@ -0,0 +1,260 @@ +package com.tangem.data.pay.repository + +import arrow.core.Either +import arrow.core.left +import com.tangem.core.error.UniversalError +import com.tangem.data.common.quote.QuotesFetcher +import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.api.pay.models.request.WithdrawDataRequest +import com.tangem.datasource.api.pay.models.request.WithdrawRequest +import com.tangem.datasource.api.pay.models.response.WithdrawResponse +import com.tangem.datasource.local.visa.TangemPayStorage +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.TangemPayWithdrawExchangeState +import com.tangem.domain.pay.TangemPayWithdrawState +import com.tangem.domain.pay.WithdrawalResult +import com.tangem.domain.pay.WithdrawalSignatureResult +import com.tangem.domain.pay.datasource.TangemPayAuthDataSource +import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.repository.CustomerOrderRepository +import com.tangem.domain.pay.repository.TangemPayWithdrawRepository +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.feature.swap.domain.api.SwapRepository +import com.tangem.feature.swap.domain.models.ExpressDataError +import com.tangem.utils.extensions.addHexPrefix +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import timber.log.Timber +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.Currency +import java.util.Locale +import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration.Companion.seconds + +private const val TAG = "TangemPaySwapRepository" + +@Suppress("LongParameterList") +internal class DefaultTangemPayWithdrawRepository @Inject constructor( + private val tangemPayApi: TangemPayApi, + private val requestHelper: TangemPayRequestPerformer, + private val authDataSource: TangemPayAuthDataSource, + private val quotesFetcher: QuotesFetcher, + private val tangemPayStorage: TangemPayStorage, + private val swapRepository: SwapRepository, + private val orderRepository: CustomerOrderRepository, +) : TangemPayWithdrawRepository { + + private val withdrawPollingScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val withdrawPollingJobs = mutableMapOf() + private val withdrawPollingMutex = Mutex() + + override suspend fun withdraw( + userWallet: UserWallet, + receiverAddress: String, + cryptoAmount: BigDecimal, + cryptoCurrencyId: CryptoCurrency.RawID, + exchangeData: TangemPayWithdrawExchangeState, + ): Either { + val amountInCents = getAmountInCents(cryptoAmount, cryptoCurrencyId) + if (amountInCents.isNullOrEmpty()) return Either.Left(VisaApiError.WithdrawalDataError) + return requestHelper.performRequest(userWallet.walletId) { authHeader -> + val request = WithdrawDataRequest(amountInCents = amountInCents, recipientAddress = receiverAddress) + tangemPayApi.getWithdrawData(authHeader = authHeader, body = request) + }.map { data -> + val result = data.result ?: return VisaApiError.WithdrawalDataError.left() + val signatureResult = authDataSource.getWithdrawalSignature( + userWallet = userWallet, + hash = result.hash, + ).getOrNull() + + return when (signatureResult) { + is WithdrawalSignatureResult.Cancelled -> { + Either.Right(WithdrawalResult.Cancelled) + } + is WithdrawalSignatureResult.Success -> { + requestHelper.performRequest(userWallet.walletId) { authHeader -> + val request = WithdrawRequest( + amountInCents = amountInCents, + recipientAddress = receiverAddress, + adminSalt = result.salt, + senderAddress = result.senderAddress, + adminSignature = signatureResult.signature.addHexPrefix(), + ) + tangemPayApi.withdraw(authHeader = authHeader, body = request) + } + .mapLeft { return Either.Left(VisaApiError.WithdrawError) } + .map { response -> + processWithdrawResult(response, userWallet, exchangeData) + WithdrawalResult.Success + } + } + null -> return Either.Left(VisaApiError.SignWithdrawError) + } + } + } + + private suspend fun processWithdrawResult( + response: WithdrawResponse, + userWallet: UserWallet, + exchangeData: TangemPayWithdrawExchangeState, + ) { + val orderId = response.result?.orderId + if (orderId != null) { + tangemPayStorage.storeActiveWithdrawOrderId(userWalletId = userWallet.walletId, orderId = orderId) + val order = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + val withdrawTxHash = order?.withdrawTxHash + val storeData = TangemPayWithdrawState( + orderId = orderId, + exchangeData = exchangeData, + txHash = withdrawTxHash, + ) + if (order != null && !withdrawTxHash.isNullOrEmpty()) { + finalizeWithdraw( + userWallet = userWallet, + txHash = withdrawTxHash, + exchangeData = exchangeData, + orderId = orderId, + ) + } else { + tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) + } + } + } + + private suspend fun finalizeWithdraw( + userWallet: UserWallet, + orderId: String, + txHash: String, + exchangeData: TangemPayWithdrawExchangeState, + ): Either { + return swapRepository.exchangeSent( + userWallet = userWallet, + txId = exchangeData.txId, + fromNetwork = exchangeData.fromNetwork, + fromAddress = exchangeData.fromAddress, + payInAddress = exchangeData.payInAddress, + txHash = txHash, + payInExtraId = exchangeData.payInExtraId, + ).also { + tangemPayStorage.deleteWithdrawOrder(userWallet.walletId, orderId) + } + } + + override suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean { + val orderId = tangemPayStorage.getActiveWithdrawOrderId(userWallet.walletId) + if (orderId.isNullOrEmpty()) return false + val orderData = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + val isActive = orderData?.status == OrderStatus.NEW || orderData?.status == OrderStatus.PROCESSING + if (!isActive) { + tangemPayStorage.deleteActiveWithdrawOrder(userWalletId = userWallet.walletId) + } + return isActive + } + + override suspend fun pollWithdrawOrdersIfNeeds(userWallet: UserWallet) { + tangemPayStorage.getWithdrawOrders(userWalletId = userWallet.walletId)?.forEach { state -> + withdrawPollingScope.launch { + try { + pollWithdrawOrderIfNeeds(userWallet = userWallet, data = state) + } catch (exception: CancellationException) { + throw exception + } catch (exception: Exception) { + Timber.tag(TAG).e(exception) + } + } + } + } + + private suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet, data: TangemPayWithdrawState) { + val exchangeData = data.exchangeData ?: return + val storedHash = data.txHash + val orderId = data.orderId + val txHash = if (storedHash.isNullOrEmpty()) { + val order = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + ?: return + order.withdrawTxHash.also { fetchedHash -> + tangemPayStorage.storeWithdrawOrder( + userWalletId = userWallet.walletId, + data = data.copy(txHash = fetchedHash), + ) + } + } else { + storedHash + } + + if (!txHash.isNullOrEmpty()) { + finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData, orderId = orderId) + } else { + startWithdrawOrderPolling(userWallet = userWallet, orderId = orderId, exchangeData = exchangeData) + } + return + } + + private suspend fun startWithdrawOrderPolling( + userWallet: UserWallet, + orderId: String, + exchangeData: TangemPayWithdrawExchangeState, + ) { + withdrawPollingMutex.withLock { + if (withdrawPollingJobs.containsKey(orderId)) return + + val pollingJob = withdrawPollingScope.launch { + try { + while (isActive) { + delay(duration = 5.seconds) + + orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId) + .onRight { order -> + val txHash = order.withdrawTxHash + if (txHash.isNullOrEmpty()) return@onRight + finalizeWithdraw( + userWallet = userWallet, + txHash = txHash, + exchangeData = exchangeData, + orderId = orderId, + ) + withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } + return@launch + } + .onLeft { error -> + Timber.tag(TAG).e("getOrderData error ${error.errorCode}") + withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } + return@launch + } + } + } catch (exception: CancellationException) { + throw exception + } catch (exception: Exception) { + Timber.tag(TAG).e(exception) + withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } + } + } + withdrawPollingJobs[orderId] = pollingJob + } + } + + private suspend fun getAmountInCents(cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID): String? { + val fiatRate = getFiatRate(cryptoCurrencyId) ?: return null + val amountInDollars = cryptoAmount.multiply(fiatRate) + val defaultFractionDigits = Currency.getInstance(Locale.US).defaultFractionDigits + return amountInDollars + .setScale(defaultFractionDigits, RoundingMode.HALF_UP) + .movePointRight(defaultFractionDigits) + .longValueExact() + .toString() + } + + private suspend fun getFiatRate(cryptoCurrencyId: CryptoCurrency.RawID): BigDecimal? { + val quotes = quotesFetcher.fetch( + fiatCurrencyId = Currency.getInstance(Locale.US).currencyCode, + currencyId = cryptoCurrencyId.value, + field = QuotesFetcher.Field.PRICE, + ).getOrNull() + + return quotes?.quotes[cryptoCurrencyId.value]?.price + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt index 36b85985ad..73f569a219 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultTangemPayWithdrawUseCase.kt @@ -4,14 +4,15 @@ import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.TangemPayWithdrawExchangeState import com.tangem.domain.pay.WithdrawalResult -import com.tangem.domain.pay.repository.TangemPaySwapRepository +import com.tangem.domain.pay.repository.TangemPayWithdrawRepository import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import java.math.BigDecimal import javax.inject.Inject internal class DefaultTangemPayWithdrawUseCase @Inject constructor( - private val repository: TangemPaySwapRepository, + private val repository: TangemPayWithdrawRepository, ) : TangemPayWithdrawUseCase { override suspend fun invoke( @@ -19,12 +20,14 @@ internal class DefaultTangemPayWithdrawUseCase @Inject constructor( cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, receiverCexAddress: String, + exchangeData: TangemPayWithdrawExchangeState, ): Either { return repository.withdraw( userWallet = userWallet, cryptoAmount = cryptoAmount, receiverAddress = receiverCexAddress, cryptoCurrencyId = cryptoCurrencyId, + exchangeData = exchangeData, ) } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStateConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStateConverter.kt new file mode 100644 index 0000000000..019ee19504 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStateConverter.kt @@ -0,0 +1,23 @@ +package com.tangem.data.pay.util + +import com.tangem.data.pay.entity.WithdrawStoreData +import com.tangem.domain.pay.TangemPayWithdrawExchangeState +import com.tangem.domain.pay.TangemPayWithdrawState +import com.tangem.utils.converter.Converter + +class WithdrawStateConverter : Converter { + + override fun convert(value: WithdrawStoreData): TangemPayWithdrawState = TangemPayWithdrawState( + orderId = value.orderId, + exchangeData = value.exchangeData?.let { exchangeData -> + TangemPayWithdrawExchangeState( + txId = exchangeData.txId, + fromNetwork = exchangeData.fromNetwork, + fromAddress = exchangeData.fromAddress, + payInAddress = exchangeData.payInAddress, + payInExtraId = exchangeData.payInExtraId, + ) + }, + txHash = value.txHash, + ) +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStoreDataConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStoreDataConverter.kt new file mode 100644 index 0000000000..5a8de01b04 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/WithdrawStoreDataConverter.kt @@ -0,0 +1,23 @@ +package com.tangem.data.pay.util + +import com.tangem.data.pay.entity.ExchangeStoreData +import com.tangem.data.pay.entity.WithdrawStoreData +import com.tangem.domain.pay.TangemPayWithdrawState +import com.tangem.utils.converter.Converter + +class WithdrawStoreDataConverter : Converter { + + override fun convert(value: TangemPayWithdrawState): WithdrawStoreData = WithdrawStoreData( + orderId = value.orderId, + exchangeData = value.exchangeData?.let { exchangeData -> + ExchangeStoreData( + txId = exchangeData.txId, + fromNetwork = exchangeData.fromNetwork, + fromAddress = exchangeData.fromAddress, + payInAddress = exchangeData.payInAddress, + payInExtraId = exchangeData.payInExtraId, + ) + }, + txHash = value.txHash, + ) +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt index 2372e3639e..d095101533 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaRepository.kt @@ -16,8 +16,9 @@ import com.tangem.data.visa.config.VisaLibLoader import com.tangem.data.visa.utils.* import com.tangem.datasource.api.visa.VisaApi import com.tangem.datasource.api.visa.models.response.VisaTxHistoryResponse -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.requireColdWallet @@ -40,7 +41,7 @@ internal class DefaultVisaRepository @Inject constructor( private val visaLibLoader: VisaLibLoader, private val quotesFetcher: QuotesFetcher, private val cacheRegistry: CacheRegistry, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val dispatchers: CoroutineDispatcherProvider, private val visaApiRequestMaker: VisaApiRequestMaker, private val visaApi: VisaApi, @@ -176,7 +177,7 @@ internal class DefaultVisaRepository @Inject constructor( } } - private suspend fun makeAddress(userWalletId: UserWalletId): String { + private fun makeAddress(userWalletId: UserWalletId): String { if (VisaConstants.IS_DEMO_MODE_ENABLED) return getDemoAddress() val userWallet = findVisaUserWallet(userWalletId) @@ -219,9 +220,7 @@ internal class DefaultVisaRepository @Inject constructor( } private fun findVisaUserWallet(userWalletId: UserWalletId): UserWallet { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "No user wallet found: $userWalletId" - } + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) if (!userWallet.requireColdWallet().scanResponse.cardTypesResolver.isVisaWallet()) { error("VISA wallet required: $userWalletId") } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt index a5f2592c55..eb91e4a601 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt @@ -7,8 +7,10 @@ import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.pay.models.request.RefreshTokenByCardWalletRequest import com.tangem.datasource.api.visa.VisaApi -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict +import com.tangem.domain.common.wallets.update import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.requireColdWallet @@ -23,7 +25,7 @@ import javax.inject.Inject typealias VisaAuthorizationHeader = String internal class VisaApiRequestMaker @Inject constructor( - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val visaAuthApi: VisaApi, private val accessCodeDataConverter: AccessCodeDataConverter, private val dispatcherProvider: CoroutineDispatcherProvider, @@ -47,11 +49,11 @@ internal class VisaApiRequestMaker @Inject constructor( val newTokens = runCatching { refreshAccessTokens(authTokens.refreshToken) - }.getOrElse { - if (it is ApiResponseError.HttpException && - it.code == ApiResponseError.HttpException.Code.UNAUTHORIZED + }.getOrElse { throwable -> + if (throwable is ApiResponseError.HttpException && + throwable.code == ApiResponseError.HttpException.Code.UNAUTHORIZED ) { - userWalletsStore.update(userWalletId) { userWallet -> + userWalletsListRepository.update(userWalletId) { userWallet -> userWallet.requireColdWallet().copy( scanResponse = userWallet.scanResponse.copy( // visaCardActivationStatus = VisaCardActivationStatus.RefreshTokenExpired, @@ -62,7 +64,7 @@ internal class VisaApiRequestMaker @Inject constructor( throw RefreshTokenExpiredException() } - userWalletsStore.update(userWalletId) { userWallet -> + userWalletsListRepository.update(userWalletId) { userWallet -> userWallet.requireColdWallet().copy( scanResponse = userWallet.scanResponse.copy( // visaCardActivationStatus = VisaCardActivationStatus.Activated( @@ -92,7 +94,7 @@ internal class VisaApiRequestMaker @Inject constructor( @Throws private fun getAuthTokens(userWalletId: UserWalletId): VisaAuthTokens { - val userWallet = findVisaUserWallet(userWalletId) + findVisaUserWallet(userWalletId) // val status = userWallet.requireColdWallet().scanResponse.visaCardActivationStatus // ?: error("Visa card activation status not found") val status: VisaCardActivationStatus = TODO("Fix visaCardActivationStatus retrieval") @@ -105,9 +107,7 @@ internal class VisaApiRequestMaker @Inject constructor( } private fun findVisaUserWallet(userWalletId: UserWalletId): UserWallet { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "No user wallet found: $userWalletId" - } + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) if (!userWallet.requireColdWallet().scanResponse.cardTypesResolver.isVisaWallet()) { error("VISA wallet required: $userWalletId") } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWalletConnectRepository.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWalletConnectRepository.kt index 62d9f25d28..4d908dbb8b 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWalletConnectRepository.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/DefaultWalletConnectRepository.kt @@ -1,22 +1,16 @@ package com.tangem.data.walletconnect -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.walletconnect.repository.WalletConnectRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext internal class DefaultWalletConnectRepository( - private val userWalletsStore: UserWalletsStore, - private val dispatchers: CoroutineDispatcherProvider, + private val userWalletsListRepository: UserWalletsListRepository, ) : WalletConnectRepository { - override suspend fun checkIsAvailable(userWalletId: UserWalletId): Boolean = withContext(dispatchers.io) { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "User wallet with id $userWalletId not found" - } - - userWallet.isMultiCurrency + override suspend fun checkIsAvailable(userWalletId: UserWalletId): Boolean { + return userWalletsListRepository.getSyncStrict(userWalletId).isMultiCurrency } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index 9b04683948..65749b77cb 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -19,12 +19,12 @@ import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.data.walletconnect.utils.WcNetworksConverter import com.tangem.data.walletconnect.utils.WcScope import com.tangem.datasource.di.SdkMoshi -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.account.supplier.SingleAccountSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.WcRequestService @@ -51,10 +51,9 @@ internal object WalletConnectDataModule { @Provides @Singleton fun providesWalletConnectRepository( - userWalletsStore: UserWalletsStore, - dispatchers: CoroutineDispatcherProvider, + userWalletsListRepository: UserWalletsListRepository, ): WalletConnectRepository { - return DefaultWalletConnectRepository(userWalletsStore, dispatchers) + return DefaultWalletConnectRepository(userWalletsListRepository) } @Provides diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index 848442eff1..8f37e4e4a1 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -9,6 +9,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.utils.* import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.walletconnect.WcAnalyticEvents @@ -116,7 +117,7 @@ internal class DefaultWcSessionsManager( val wcSessions = savedPending.plus(inStore).mapNotNull { storeSession -> val wallet = wallets.find { it.walletId == storeSession.walletId } ?: return@mapNotNull null val sdkSession = inSdk.find { it.topic == storeSession.topic } ?: return@mapNotNull null - val account = storeSession.accountId?.let { wcNetworksConverter.getAccount(it) } + val account = storeSession.accountId?.let { wcNetworksConverter.getAccount(it) } as? Account.CryptoPortfolio if (accountsFeatureToggles.isFeatureEnabled && account == null) { return@mapNotNull null } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt index 346ba94376..ebf56dafcf 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt @@ -5,7 +5,6 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.sign.SignStateConverter.toPreSign import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.SignStateConverter.toSigning -import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcRequestError import com.tangem.domain.walletconnect.model.WcRequestError.Companion.code diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt index 82a5b0beb2..d4b19d974a 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt @@ -13,6 +13,7 @@ import com.tangem.domain.account.supplier.SingleAccountSupplier import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet @@ -146,10 +147,12 @@ internal class WcNetworksConverter @Inject constructor( .filterIsInstance().map(CryptoCurrency.Coin::network) } - private suspend fun getAccountStatus(accountId: AccountId): AccountStatus? { + private suspend fun getAccountStatus(accountId: AccountId): AccountStatus.CryptoPortfolio? { return singleAccountStatusListSupplier.getSyncOrNull( SingleAccountStatusListProducer.Params(accountId.userWalletId), - )?.accountStatuses?.find { it.account.accountId == accountId } + )?.accountStatuses + ?.filterCryptoPortfolio() + ?.find { it.account.accountId == accountId } } suspend fun getAccountNetworks(accountId: AccountId): List { diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt index b2b04e77fa..d10812bf6b 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt @@ -28,8 +28,9 @@ import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.walletmanager.utils.* import com.tangem.datasource.asset.loader.AssetLoader -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -60,7 +61,7 @@ import javax.inject.Inject @Suppress("LargeClass", "TooManyFunctions") internal class DefaultWalletManagersFacade @Inject constructor( private val walletManagersStore: WalletManagersStore, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val assetLoader: AssetLoader, private val dispatchers: CoroutineDispatcherProvider, private val gaslessTransactionRepository: GaslessTransactionRepository, @@ -300,7 +301,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( } } - private fun getUserWallet(userWalletId: UserWalletId) = userWalletsStore.getSyncStrict(userWalletId) + private fun getUserWallet(userWalletId: UserWalletId) = userWalletsListRepository.getSyncStrict(userWalletId) private suspend fun getAndUpdateWalletManager( userWallet: UserWallet, diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index aafa1080db..512c474032 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -10,6 +10,10 @@ android { namespace = "com.tangem.data.wallet" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { implementation(projects.data.common) @@ -47,9 +51,7 @@ dependencies { implementation(deps.timber) /** tests */ + testImplementation(projects.test.core) testImplementation(projects.common.test) - testImplementation(deps.test.junit) - testImplementation(deps.test.coroutine) - testImplementation(deps.test.truth) - testImplementation(deps.test.mockk) + testRuntimeOnly(deps.test.junit5.engine) } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsPromoRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsPromoRepository.kt index ea64bbd90d..e5fb0f7e81 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsPromoRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsPromoRepository.kt @@ -10,7 +10,7 @@ import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.storeObject -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.wallets.models.AppsFlyerConversionData import com.tangem.domain.wallets.repository.WalletsPromoRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -20,7 +20,7 @@ import timber.log.Timber internal class DefaultWalletsPromoRepository( private val appPreferencesStore: AppPreferencesStore, private val tangemTechApi: TangemTechApi, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val dispatchers: CoroutineDispatcherProvider, private val appsFlyerStore: AppsFlyerStore, ) : WalletsPromoRepository { @@ -66,7 +66,7 @@ internal class DefaultWalletsPromoRepository( } private suspend fun bind(refcode: String, campaign: String?) { - val walletIds = userWalletsStore.userWalletsSync.map { it.walletId.stringValue } + val walletIds = userWalletsListRepository.userWallets.value.orEmpty().map { it.walletId.stringValue } val result = tangemTechApi.bindWalletsByReferralCode( body = BindWalletsByReferralCodeBody(walletIds = walletIds, refcode = refcode, campaign = campaign), diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index ea7ba4b3b1..83af408c47 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -20,9 +20,14 @@ 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.userwallet.UserWalletsStore +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.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncOrNull +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus @@ -44,7 +49,7 @@ typealias SeedPhraseNotificationsStatuses = Map, private val dispatchers: CoroutineDispatcherProvider, private val authProvider: AuthProvider, @@ -54,21 +59,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, @@ -163,7 +153,7 @@ internal class DefaultWalletsRepository( } private suspend fun fetchSeedPhraseNotificationStatus(userWalletId: UserWalletId) { - val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) + val userWallet = userWalletsListRepository.getSyncOrNull(id = userWalletId) if (userWallet != null && userWallet !is UserWallet.Cold) { updateNotificationVisibility(id = userWalletId, value = SeedPhraseNotificationsStatus.NOT_NEEDED) @@ -335,7 +325,7 @@ internal class DefaultWalletsRepository( } override suspend fun setWalletName(walletId: UserWalletId, walletName: String) = withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncOrNull(key = walletId) + val userWallet = userWalletsListRepository.getSyncOrNull(id = walletId) tangemTechApi.updateWallet( walletId = walletId.stringValue, @@ -344,7 +334,7 @@ internal class DefaultWalletsRepository( } override suspend fun upgradeWallet(walletId: UserWalletId) = withContext(dispatchers.io) { - val userWallet = userWalletsStore.getSyncStrict(key = walletId) + val userWallet = userWalletsListRepository.getSyncStrict(id = walletId) tangemTechApi.updateWallet( walletId = walletId.stringValue, diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt index 3a3dcf27ac..534b16cb36 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt @@ -1,9 +1,10 @@ package com.tangem.data.wallets.derivations -import com.tangem.common.CompletionResult +import arrow.core.getOrElse import com.tangem.common.extensions.ByteArrayKey import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -16,16 +17,22 @@ import com.tangem.domain.wallets.usecase.BackendId import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext +import timber.log.Timber import javax.inject.Inject internal class DefaultDerivationsRepository @Inject constructor( - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val hotDerivationsRepository: HotMapDerivationsRepository, private val coldDerivationsRepository: ColdMapDerivationsRepository, private val dispatchers: CoroutineDispatcherProvider, ) : DerivationsRepository { override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List) { + if (currencies.isEmpty()) { + Timber.d("Nothing to derive") + return + } + derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network)) } @@ -34,7 +41,7 @@ internal class DefaultDerivationsRepository @Inject constructor( networkIds: List, accountIndex: DerivationIndex, ) { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) when (userWallet) { is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeysByNetworkIds(userWallet, networkIds) is UserWallet.Hot -> { @@ -46,7 +53,7 @@ internal class DefaultDerivationsRepository @Inject constructor( } override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List) { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) when (userWallet) { is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeysByNetworks(userWallet, networks) is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeysByNetworks(userWallet, networks) @@ -59,7 +66,7 @@ internal class DefaultDerivationsRepository @Inject constructor( userWalletId: UserWalletId, derivations: Map>, ): Map { - val userWallet = userWalletsStore.getSyncStrict(userWalletId) + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) return when (userWallet) { is UserWallet.Cold -> coldDerivationsRepository.derivePublicKeys(userWallet, derivations) is UserWallet.Hot -> hotDerivationsRepository.derivePublicKeys(userWallet, derivations) @@ -73,7 +80,7 @@ internal class DefaultDerivationsRepository @Inject constructor( userWalletId: UserWalletId, networksWithDerivationPath: Map, ): Boolean { - return when (val userWallet = userWalletsStore.getSyncStrict(userWalletId)) { + return when (val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)) { is UserWallet.Cold -> coldDerivationsRepository.hasMissedDerivations(userWallet, networksWithDerivationPath) is UserWallet.Hot -> hotDerivationsRepository.hasMissedDerivations(userWallet, networksWithDerivationPath) } @@ -88,14 +95,7 @@ internal class DefaultDerivationsRepository @Inject constructor( return@withContext // No update needed } - val updateResult = userWalletsStore.update( - userWalletId = newUserWallet.walletId, - update = { userWalletToUpdate -> newUserWallet }, - ) - - when (updateResult) { - is CompletionResult.Failure -> throw updateResult.error - is CompletionResult.Success -> updateResult.data - } + userWalletsListRepository.saveWithoutLock(userWallet = newUserWallet, canOverride = true) + .getOrElse { throw IllegalStateException("Unable to update user wallet: $it") } } } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt index 6eb367a7f4..c580d60ae1 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -15,8 +15,8 @@ import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.domain.wallets.derivations.HotMapDerivationsRepository @@ -41,7 +41,7 @@ internal object WalletsDataModule { fun providesWalletsRepository( appPreferencesStore: AppPreferencesStore, tangemTechApi: TangemTechApi, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, dispatchers: CoroutineDispatcherProvider, authProvider: AuthProvider, walletServerBinder: WalletServerBinder, @@ -52,7 +52,7 @@ internal object WalletsDataModule { return DefaultWalletsRepository( appPreferencesStore = appPreferencesStore, tangemTechApi = tangemTechApi, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()), dispatchers = dispatchers, authProvider = authProvider, @@ -74,14 +74,14 @@ internal object WalletsDataModule { fun provideWalletsPromoRepository( appPreferencesStore: AppPreferencesStore, tangemTechApi: TangemTechApi, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, dispatchers: CoroutineDispatcherProvider, appsFlyerStore: AppsFlyerStore, ): WalletsPromoRepository { return DefaultWalletsPromoRepository( appPreferencesStore = appPreferencesStore, tangemTechApi = tangemTechApi, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, dispatchers = dispatchers, appsFlyerStore = appsFlyerStore, ) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt index 987dd65cc5..8d0f7ec5d1 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotMapDerivationsRepository.kt @@ -7,7 +7,8 @@ import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.common.network.NetworkFactory import com.tangem.data.wallets.derivations.MissedDerivationsFinder -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -23,7 +24,7 @@ import timber.log.Timber import javax.inject.Inject internal class DefaultHotMapDerivationsRepository @Inject constructor( - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, private val networkFactory: NetworkFactory, private val hotWalletAccessor: HotWalletAccessor, private val dispatchers: CoroutineDispatcherProvider, @@ -90,7 +91,7 @@ internal class DefaultHotMapDerivationsRepository @Inject constructor( // Get the updated user wallet from the store to ensure we have the latest data // in case it was modified during the derive operation - val updatedUserWallet = userWalletsStore.getSyncStrict(userWallet.walletId) as UserWallet.Hot + val updatedUserWallet = userWalletsListRepository.getSyncStrict(userWallet.walletId) as UserWallet.Hot val newKeys = result.responses.associate { ByteArrayKey(it.seedKey.publicKey) to ExtendedPublicKeysMap(it.publicKeys) } diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt index 30dd508d5b..71182a19c0 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -54,7 +54,7 @@ class DefaultWalletsRepositoryTest { repository = DefaultWalletsRepository( appPreferencesStore = appPreferenceStore, tangemTechApi = tangemTechApi, - userWalletsStore = mockk(), + userWalletsListRepository = mockk(), seedPhraseNotificationVisibilityStore = mockk(), dispatchers = dispatchers, authProvider = mockk(), @@ -222,7 +222,7 @@ class DefaultWalletsRepositoryTest { repository = DefaultWalletsRepository( appPreferencesStore = appPreferenceStore, tangemTechApi = tangemTechApi, - userWalletsStore = mockk(), + userWalletsListRepository = mockk(), seedPhraseNotificationVisibilityStore = mockk(), dispatchers = dispatchers, authProvider = authProvider, diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt index e9bccfd5a5..3f759cd81e 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepositoryTest.kt @@ -1,44 +1,37 @@ package com.tangem.data.wallets.derivations -import android.annotation.SuppressLint +import arrow.core.right import com.google.common.truth.Truth -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.common.CompletionResult import com.tangem.common.test.domain.card.MockScanResponseFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory -import com.tangem.data.common.network.NetworkFactory -import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.card.ScanCardException import com.tangem.domain.card.configs.GenericCardConfig -import com.tangem.domain.card.configs.MultiWalletCardConfig +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.operations.derivation.DerivationTaskResponse -import com.tangem.sdk.api.TangemSdkManager +import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk +import io.mockk.* +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest -import org.junit.Test +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance /** [REDACTED_AUTHOR] */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class DefaultDerivationsRepositoryTest { - private val tangemSdkManager = mockk() - private val userWalletsStore = mockk() + private val userWalletsListRepository = mockk() + private val coldDerivationsRepository: ColdMapDerivationsRepository = mockk() + private val repository = DefaultDerivationsRepository( - userWalletsStore = userWalletsStore, - dispatchers = TestingCoroutineDispatcherProvider(), + userWalletsListRepository = userWalletsListRepository, hotDerivationsRepository = mockk(), - coldDerivationsRepository = DefaultColdMapDerivationsRepository( - tangemSdkManager = tangemSdkManager, - networkFactory = NetworkFactory(excludedBlockchains = ExcludedBlockchains()), - dispatchers = TestingCoroutineDispatcherProvider(), - ), + coldDerivationsRepository = coldDerivationsRepository, + dispatchers = TestingCoroutineDispatcherProvider(), ) private val defaultUserWalletId = UserWalletId("011") @@ -51,127 +44,104 @@ internal class DefaultDerivationsRepositoryTest { hasBackupError = false, ) + @AfterEach + fun tearDown() { + clearMocks(userWalletsListRepository, coldDerivationsRepository) + } + @Test fun `error if userWalletId not found`() = runTest { - coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } throws IllegalStateException() + val currencies = MockCryptoCurrencyFactory(defaultUserWallet).ethereum.let(::listOf) + val userWalletsFlow = MutableStateFlow?>(null) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow runCatching { - repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) + repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = currencies) + } + .onSuccess { error("Should throws exception") } + .onFailure { + Truth.assertThat(it).isInstanceOf(IllegalArgumentException::class.java) + Truth.assertThat(it).hasMessageThat() + .isEqualTo("Unable to find user wallet with provided ID: $defaultUserWalletId") + } + + coVerify(exactly = 1) { userWalletsListRepository.userWallets } + coVerify(inverse = true) { + coldDerivationsRepository.derivePublicKeysByNetworks(any(), any()) + userWalletsListRepository.saveWithoutLock(any(), any()) + } + } + + @Test + fun `success if currencies is empty`() = runTest { + repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) + + coVerify(inverse = true) { + userWalletsListRepository.userWallets + coldDerivationsRepository.derivePublicKeysByNetworks(any(), any()) + userWalletsListRepository.saveWithoutLock(any(), any()) + } + } + + @Test + fun `error if coldDerivationsRepository throws exception`() = runTest { + val currencies = MockCryptoCurrencyFactory(defaultUserWallet).ethereum.let(::listOf) + val userWalletsFlow = MutableStateFlow(listOf(defaultUserWallet)) + + every { userWalletsListRepository.userWallets } returns userWalletsFlow + + coEvery { + coldDerivationsRepository.derivePublicKeysByNetworks( + userWallet = defaultUserWallet, + networks = any(), + ) + } throws IllegalStateException() + + runCatching { + repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = currencies) } .onSuccess { error("Should throws exception") } .onFailure { Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java) } - coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } - coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) } - coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } - } - - @SuppressLint("CheckResult") - @Test - fun `success if card is not supported derivations`() = runTest { - coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns defaultUserWallet - - repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) - - runCatching { } - .onSuccess { Truth.assertThat(it) } - .onFailure { - error("Should returns success") - } - - coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } - coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) } - coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } - } - - @SuppressLint("CheckResult") - @Test - fun `success if currencies is empty`() = runTest { - val userWallet = defaultUserWallet.copy( - scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), - ) - coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet - - runCatching { repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = emptyList()) } - .onSuccess { Truth.assertThat(it) } - .onFailure { error("Should returns success") } - - coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } - coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) } - coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } - } - - @SuppressLint("CheckResult") - @Test - fun `success if card already has derivations`() = runTest { - val userWallet = defaultUserWallet.copy( - scanResponse = MockScanResponseFactory.create( - cardConfig = MultiWalletCardConfig, - derivedKeys = DerivedKeysMocks.ethereumDerivedKeys, - ), - ) - - coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet - - runCatching { - repository.derivePublicKeys( - userWalletId = defaultUserWalletId, - currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf), + coVerifyOrder { + userWalletsListRepository.userWallets + coldDerivationsRepository.derivePublicKeysByNetworks( + userWallet = defaultUserWallet, + networks = currencies.map(CryptoCurrency.Coin::network), ) } - .onSuccess { Truth.assertThat(it) } - .onFailure { error("Should returns success") } - coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } - coVerify(inverse = true) { tangemSdkManager.derivePublicKeys(null, any(), any()) } - coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } + coVerify(inverse = true) { userWalletsListRepository.saveWithoutLock(any(), any()) } } - @Test - fun `error if tangemSdkManager throws exception`() = runTest { - val userWallet = defaultUserWallet.copy( - scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), - ) - coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet - coEvery { tangemSdkManager.derivePublicKeys(null, any(), any()) } throws ScanCardException.UserCancelled() - - runCatching { - repository.derivePublicKeys( - userWalletId = defaultUserWalletId, - currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf), - ) - } - .onSuccess { error("Should throws exception") } - .onFailure { Truth.assertThat(it).isInstanceOf(ScanCardException.UserCancelled::class.java) } - - coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } - coVerify(exactly = 1) { tangemSdkManager.derivePublicKeys(null, any(), any()) } - coVerify(inverse = true) { userWalletsStore.update(defaultUserWalletId, any()) } - } - - @SuppressLint("CheckResult") @Test fun `success case`() = runTest { - val userWallet = defaultUserWallet.copy( - scanResponse = MockScanResponseFactory.create(cardConfig = MultiWalletCardConfig, derivedKeys = emptyMap()), - ) - coEvery { userWalletsStore.getSyncStrict(defaultUserWalletId) } returns userWallet - coEvery { tangemSdkManager.derivePublicKeys(null, any(), any()) } returns CompletionResult.Success( - DerivationTaskResponse(DerivedKeysMocks.ethereumDerivedKeys), - ) - coEvery { userWalletsStore.update(defaultUserWalletId, any()) } returns CompletionResult.Success(userWallet) + val currencies = MockCryptoCurrencyFactory(defaultUserWallet).ethereum.let(::listOf) + val userWalletsFlow = MutableStateFlow(listOf(defaultUserWallet)) + val updatedWallet = defaultUserWallet.copy(cardsInWallet = setOf("AC01")) - runCatching { - repository.derivePublicKeys( - userWalletId = defaultUserWalletId, - currencies = MockCryptoCurrencyFactory(userWallet).ethereum.let(::listOf), + every { userWalletsListRepository.userWallets } returns userWalletsFlow + + coEvery { + coldDerivationsRepository.derivePublicKeysByNetworks( + userWallet = defaultUserWallet, + networks = currencies.map(CryptoCurrency.Coin::network), ) - } - .onSuccess { Truth.assertThat(it) } - .onFailure { error("Should returns success but $it") } + } returns updatedWallet + coEvery { + userWalletsListRepository.saveWithoutLock(updatedWallet, true) + } returns updatedWallet.right() - coVerify(exactly = 1) { userWalletsStore.getSyncStrict(defaultUserWalletId) } - coVerify(exactly = 1) { tangemSdkManager.derivePublicKeys(null, any(), any()) } - coVerify(exactly = 1) { userWalletsStore.update(defaultUserWalletId, any()) } + repository.derivePublicKeys(userWalletId = defaultUserWalletId, currencies = currencies) + + coVerifyOrder { + userWalletsListRepository.userWallets + coldDerivationsRepository.derivePublicKeysByNetworks( + userWallet = defaultUserWallet, + networks = currencies.map(CryptoCurrency.Coin::network), + ) + userWalletsListRepository.saveWithoutLock(updatedWallet, true) + } } } \ No newline at end of file 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/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index cb0f812e98..c413c7e46f 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -235,7 +235,7 @@ data class AccountList private constructor( */ fun empty( userWalletId: UserWalletId, - cryptoCurrencies: Set = emptySet(), + cryptoCurrencies: List = emptyList(), sortType: TokensSortType = TokensSortType.NONE, groupType: TokensGroupType = TokensGroupType.NONE, ): AccountList { diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt index 8309ed3403..cc3b4c4204 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt @@ -5,6 +5,7 @@ import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import kotlinx.serialization.Serializable @@ -33,13 +34,16 @@ data class AccountStatusList( ) { val mainAccount: AccountStatus.CryptoPortfolio - get() = accountStatuses.first { accountStatus -> - when (accountStatus) { - is AccountStatus.CryptoPortfolio -> accountStatus.account.isMainAccount + get() = accountStatuses + .filterCryptoPortfolio() + .first { accountStatus -> + when (accountStatus) { + is AccountStatus.CryptoPortfolio -> accountStatus.account.isMainAccount + } } - } as AccountStatus.CryptoPortfolio fun flattenCurrencies(): List = accountStatuses + .filterCryptoPortfolio() .map { accountStatus -> accountStatus.flattenCurrencies() } .flatten() diff --git a/domain/account/src/main/java/com/tangem/domain/account/producer/SingleAccountProducer.kt b/domain/account/src/main/java/com/tangem/domain/account/producer/SingleAccountProducer.kt index 541ce35936..76a318a696 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/producer/SingleAccountProducer.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/producer/SingleAccountProducer.kt @@ -5,10 +5,10 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId /** - * Produces a flow of [Account.CryptoPortfolio] for a single account identified by [Params.accountId]. + * Produces a flow of [Account] for a single account identified by [Params.accountId]. * The flow emits updates whenever the account's portfolio changes. */ -interface SingleAccountProducer : FlowProducer { +interface SingleAccountProducer : FlowProducer { data class Params(val accountId: AccountId) diff --git a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt index 8967491701..5e76e84653 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/repository/AccountsCRUDRepository.kt @@ -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 @@ -29,7 +28,7 @@ interface AccountsCRUDRepository { * Retrieves a specific account by its unique identifier * * @param accountId the unique identifier of the account - * @return an [Option] containing the [Account.CryptoPortfolio] if found, or `Option.None` if not + * @return an [Option] containing the [Account] if found, or `Option.None` if not */ suspend fun getAccountSync(accountId: AccountId): Option @@ -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/supplier/SingleAccountSupplier.kt b/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt index 527a32f557..2a87c8b14e 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt @@ -3,9 +3,12 @@ package com.tangem.domain.account.supplier import com.tangem.domain.account.producer.SingleAccountProducer import com.tangem.domain.core.flow.FlowCachingSupplier import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterIsInstance /** - * Supplies instances of [SingleAccountProducer] that produce flows of [Account.CryptoPortfolio] + * Supplies instances of [SingleAccountProducer] that produce flows of [Account] * for individual accounts. Each producer is uniquely identified by its [SingleAccountProducer.Params]. * * @property factory A factory to create instances of [SingleAccountProducer]. @@ -14,4 +17,9 @@ import com.tangem.domain.models.account.Account abstract class SingleAccountSupplier( override val factory: SingleAccountProducer.Factory, override val keyCreator: (SingleAccountProducer.Params) -> String, -) : FlowCachingSupplier() \ No newline at end of file +) : FlowCachingSupplier() { + + fun filterPaymentAccount(accountId: AccountId): Flow { + return invoke(params = SingleAccountProducer.Params(accountId)).filterIsInstance() + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt index cf7992e6ab..aa598d03d9 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCase.kt @@ -81,7 +81,7 @@ class AddCryptoPortfolioUseCase( accountName = accountName, icon = icon, derivationIndex = derivationIndex, - cryptoCurrencies = emptySet(), + cryptoCurrencies = emptyList(), ) } diff --git a/domain/account/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..2a0bfba4d0 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt @@ -4,6 +4,8 @@ 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.common.wallets.loadAndGet import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -14,12 +16,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 +32,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 +45,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 +55,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..750b29926b 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt @@ -5,11 +5,12 @@ 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 import io.mockk.* -import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOf @@ -24,13 +25,18 @@ import org.junit.jupiter.api.TestInstance class IsAccountsModeEnabledUseCaseTest { private val accountsCRUDRepository: AccountsCRUDRepository = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) 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,36 +55,19 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isFalse() verify(exactly = 1) { featureToggles.isFeatureEnabled } - verify(inverse = true) { accountsCRUDRepository.getUserWallets() } - } - - @Test - fun `returns false when getUserWallets emits empty flow`() = runTest { - // Arrange - every { featureToggles.isFeatureEnabled } returns true - every { accountsCRUDRepository.getUserWallets() } returns emptyFlow() - - // Act - val actual = useCase.invoke().firstOrNull() - - // Assert - Truth.assertThat(actual).isFalse() - - verifyOrder { - featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWallets() + coVerify(inverse = true) { + userWalletsListRepository.load() + userWalletsListRepository.userWallets } - - 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.userWallets } returns MutableStateFlow(listOf(wallet)) // Act val actual = useCase.invoke().first() @@ -86,21 +75,22 @@ class IsAccountsModeEnabledUseCaseTest { // Assert Truth.assertThat(actual).isFalse() - verifyOrder { + coVerifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWallets() + userWalletsListRepository.load() + userWalletsListRepository.userWallets } 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.userWallets } returns MutableStateFlow(listOf(wallet)) every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } returns flowOf(2.some()) // Act @@ -109,20 +99,21 @@ class IsAccountsModeEnabledUseCaseTest { // Assert Truth.assertThat(actual).isTrue() - verifyOrder { + coVerifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWallets() + userWalletsListRepository.load() + userWalletsListRepository.userWallets 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.userWallets } returns MutableStateFlow(listOf(wallet)) every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } returns flowOf(none()) // Act @@ -131,21 +122,22 @@ class IsAccountsModeEnabledUseCaseTest { // Assert Truth.assertThat(actual).isFalse() - verifyOrder { + coVerifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWallets() + userWalletsListRepository.load() + userWalletsListRepository.userWallets 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.userWallets } returns MutableStateFlow(listOf(wallet1, wallet2)) every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet2.walletId) } returns flowOf(2.some()) // Act @@ -154,9 +146,10 @@ class IsAccountsModeEnabledUseCaseTest { // Assert Truth.assertThat(actual).isTrue() - verifyOrder { + coVerifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWallets() + userWalletsListRepository.load() + userWalletsListRepository.userWallets accountsCRUDRepository.getTotalActiveAccountsCount(wallet2.walletId) } @@ -180,14 +173,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 +190,7 @@ class IsAccountsModeEnabledUseCaseTest { verifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value } coVerify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCountSync(any()) } @@ -209,7 +202,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 +212,7 @@ class IsAccountsModeEnabledUseCaseTest { verifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value } coVerify(inverse = true) { accountsCRUDRepository.getTotalActiveAccountsCountSync(any()) } @@ -231,7 +224,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 +235,7 @@ class IsAccountsModeEnabledUseCaseTest { coVerifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } } @@ -253,7 +246,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 +257,7 @@ class IsAccountsModeEnabledUseCaseTest { coVerifyOrder { featureToggles.isFeatureEnabled - accountsCRUDRepository.getUserWalletsSync() + userWalletsListRepository.userWallets.value accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } } @@ -276,7 +269,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 +280,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..edecc9f2c4 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducer.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducer.kt @@ -3,8 +3,9 @@ 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.common.wallets.loadAndGet import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted @@ -20,7 +21,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 +31,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 +40,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..be5c3f927d 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt @@ -7,8 +7,9 @@ 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.common.wallets.getSyncStrict import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceLoading @@ -55,10 +56,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 +73,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 +95,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, @@ -288,6 +295,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo return map { accountStatus -> when (accountStatus) { is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance + is AccountStatus.Payment -> accountStatus.totalFiatBalance } } } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt index f3a5fdeec0..6dc53cca47 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt @@ -14,8 +14,6 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.error.TokenListSortingError import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import timber.log.Timber @@ -137,7 +135,6 @@ class ApplyTokenListSortingUseCaseV2( errors[account.accountId] = it return@map account } - .toSet() account.copy(cryptoCurrencies = accountCurrencies) } @@ -171,17 +168,13 @@ class ApplyTokenListSortingUseCaseV2( private suspend fun Raise.applySorting(accountList: AccountList) { coroutineScope { - val results = awaitAll( - async { - Either.catch { accountsCRUDRepository.saveAccountsLocally(accountList) } - }, - async { - Either.catch { accountsCRUDRepository.syncTokens(accountList.userWalletId) } - }, - ) + val results = Either.catch { + accountsCRUDRepository.saveAccountsLocally(accountList) + accountsCRUDRepository.syncTokens(accountList.userWalletId) + } - ensure(results.none { it.isLeft() }) { - val message = results.mapNotNull { it.leftOrNull() }.joinToString() + ensure(results.isRight()) { + val message = results.leftOrNull() raise(TokenListSortingError.DataError(IllegalStateException(message))) } } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCase.kt index 7a0c57b6ac..de9a515a80 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCase.kt @@ -9,7 +9,7 @@ import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.models.account.AccountId -import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.referral.ReferralRepository @@ -68,7 +68,7 @@ class ArchiveCryptoPortfolioUseCase( val account = accountStatusList.accountStatuses .asSequence() - .filterIsInstance() + .filterCryptoPortfolio() .firstOrNull { it.accountId == accountId } ensureNotNull(account) { diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/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..c0148890fb 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetCryptoCurrencyActionsUseCaseV2.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetCryptoCurrencyActionsUseCaseV2.kt @@ -1,9 +1,10 @@ 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.common.wallets.getSyncStrict import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase @@ -17,14 +18,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 +46,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/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt index dd64e3d0cc..2774fa49fe 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt @@ -59,14 +59,21 @@ class ManageCryptoCurrenciesUseCase( accountId: AccountId, add: CryptoCurrency? = null, remove: CryptoCurrency? = null, + skipDerivationErrors: Boolean = true, ): Either { - return invoke(accountId = accountId, add = listOfNotNull(add), remove = listOfNotNull(remove)) + return invoke( + accountId = accountId, + add = listOfNotNull(add), + remove = listOfNotNull(remove), + skipDerivationErrors = skipDerivationErrors, + ) } suspend operator fun invoke( accountId: AccountId, add: List = emptyList(), remove: List = emptyList(), + skipDerivationErrors: Boolean = true, ): Either = eitherOn(dispatchers.default) { if (add.isEmpty() && remove.isEmpty()) { Timber.d("No currencies to add or remove, skipping") @@ -77,7 +84,7 @@ class ManageCryptoCurrenciesUseCase( withContext(NonCancellable) { val accountStatus = getAccountStatus(accountId = accountId) - val modifiedCurrencyList = accountStatus.tokenList.flattenCurrencies() + var modifiedCurrencyList = accountStatus.tokenList.flattenCurrencies() .modify(add = add, remove = remove) if (!modifiedCurrencyList.hasChanges) { @@ -85,11 +92,20 @@ class ManageCryptoCurrenciesUseCase( return@withContext } - saveAccount( - account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet()), - ) + val derivingResult = derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) - derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) + if (!skipDerivationErrors && derivingResult.isLeft()) { + modifiedCurrencyList = accountStatus.tokenList.flattenCurrencies() + .modify(add = emptyList(), remove = remove) + + if (!modifiedCurrencyList.hasChanges) { + derivingResult.bind() + } + } + + saveAccount( + account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total), + ) parallelUpdatingScope.launch { syncTokens(userWalletId, modifiedCurrencyList) @@ -98,6 +114,10 @@ class ManageCryptoCurrenciesUseCase( refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total) clearMetadata(userWalletId = userWalletId, currencies = modifiedCurrencyList.removed) } + + if (!skipDerivationErrors) { + derivingResult.bind() + } } } @@ -126,7 +146,7 @@ class ManageCryptoCurrenciesUseCase( val modifiedCurrencyList = accountStatus.tokenList.flattenCurrencies() .modify(add = listOf(tokenToAdd)) - saveAccount(account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total.toSet())) + saveAccount(account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total)) parallelUpdatingScope.launch { syncTokens(userWalletId, modifiedCurrencyList) diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt index c972f97b80..36a47dc929 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCase.kt @@ -100,7 +100,7 @@ class RecoverCryptoPortfolioUseCase( accountName = this.name, icon = this.icon, derivationIndex = this.derivationIndex, - cryptoCurrencies = emptySet(), + cryptoCurrencies = emptyList(), ) } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2.kt index 3583eee3d2..32e1b28366 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2.kt @@ -48,10 +48,12 @@ class ToggleTokenListGroupingUseCaseV2( account.copy(tokenList = account.tokenList.reverseGroupType()) } + val updatedCryptoAccount = + updatedAccountList.firstOrNull { it is AccountStatus.CryptoPortfolio } as? AccountStatus.CryptoPortfolio accountStatusList.copy( accountStatuses = updatedAccountList, - groupType = when (updatedAccountList.firstOrNull()?.getCryptoTokenList()) { + groupType = when (updatedCryptoAccount?.tokenList) { is TokenList.GroupedByNetwork -> TokensGroupType.NETWORK is TokenList.Ungrouped -> TokensGroupType.NONE else -> accountStatusList.groupType diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/AccountCryptoCurrencyStatusFinder.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/AccountCryptoCurrencyStatusFinder.kt index dc33b171dd..fe5daf41d0 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/AccountCryptoCurrencyStatusFinder.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/AccountCryptoCurrencyStatusFinder.kt @@ -7,6 +7,7 @@ import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatuses import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.lib.crypto.derivation.AccountNodeRecognizer @@ -49,7 +50,7 @@ internal object AccountCryptoCurrencyStatusFinder { ): AccountCryptoCurrencyStatus? { return accountStatusList.getExpectedAccountStatuses(network) .asSequence() - .filterIsInstance() + .filterCryptoPortfolio() .mapNotNull { accountStatus -> val status = accountStatus.flattenCurrencies().firstOrNull { it.currency.id == currencyId } ?: return@mapNotNull null @@ -78,7 +79,7 @@ internal object AccountCryptoCurrencyStatusFinder { return accountStatusList.getExpectedAccountStatuses(networks) .asSequence() - .filterIsInstance() + .filterCryptoPortfolio() .associate { accountStatus -> val statuses = accountStatus.flattenCurrencies() .filter { it.currency.id in currencyIds } @@ -108,7 +109,7 @@ internal object AccountCryptoCurrencyStatusFinder { derivationPath = derivationPath, ) .asSequence() - .filterIsInstance() + .filterCryptoPortfolio() .mapNotNull { accountStatus -> val status = accountStatus.flattenCurrencies().firstOrNull { val currency = it.currency @@ -156,8 +157,8 @@ internal object AccountCryptoCurrencyStatusFinder { DerivationIndex.Main.value -> listOf(mainAccount) // currency only in the account with specific derivation index or in the main account else -> { - val accountStatus = accountStatuses.firstOrNull { - val cryptoPortfolio = it.account as? Account.CryptoPortfolio ?: return@firstOrNull false + val accountStatus = accountStatuses.firstOrNull { accountStatus -> + val cryptoPortfolio = accountStatus.account as? Account.CryptoPortfolio ?: return@firstOrNull false cryptoPortfolio.derivationIndex.value == possibleAccountIndex } @@ -172,8 +173,8 @@ internal object AccountCryptoCurrencyStatusFinder { if (possibleAccountIndexes.isEmpty()) return this@getExpectedAccountStatuses.accountStatuses - val accountStatuses = this@getExpectedAccountStatuses.accountStatuses.filter { - val cryptoPortfolio = it.account as? Account.CryptoPortfolio ?: return@filter false + val accountStatuses = this@getExpectedAccountStatuses.accountStatuses.filter { accountStatus -> + val cryptoPortfolio = accountStatus.account as? Account.CryptoPortfolio ?: return@filter false cryptoPortfolio.derivationIndex.value in possibleAccountIndexes } diff --git a/domain/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..67ed61d8e6 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt @@ -2,14 +2,17 @@ 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 import com.tangem.test.core.getEmittedValues import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.* +import io.mockk.clearMocks +import io.mockk.coVerifySequence +import io.mockk.every +import io.mockk.mockk import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -21,7 +24,7 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultMultiAccountStatusListProducerTest { - private val accountsCRUDRepository: AccountsCRUDRepository = mockk() + private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier = mockk() private val dispatchers = TestingCoroutineDispatcherProvider() private val flowProducerTools: FlowProducerTools = mockk() @@ -38,7 +41,7 @@ class DefaultMultiAccountStatusListProducerTest { private val producer = DefaultMultiAccountStatusListProducer( params = Unit, - accountsCRUDRepository = accountsCRUDRepository, + userWalletsListRepository = userWalletsListRepository, singleAccountStatusListSupplier = singleAccountStatusListSupplier, dispatchers = dispatchers, flowProducerTools = flowProducerTools, @@ -46,7 +49,7 @@ class DefaultMultiAccountStatusListProducerTest { @AfterEach fun tearDown() { - clearMocks(accountsCRUDRepository, singleAccountStatusListSupplier) + clearMocks(userWalletsListRepository, singleAccountStatusListSupplier) } @Test @@ -55,7 +58,7 @@ class DefaultMultiAccountStatusListProducerTest { val wallets = listOf(userWallet1, userWallet2) val walletsFlow = MutableStateFlow(wallets) - every { accountsCRUDRepository.getUserWallets() } returns walletsFlow + every { userWalletsListRepository.userWallets } returns walletsFlow val accountStatusList1 = mockk() val accountStatusList2 = mockk() @@ -78,8 +81,9 @@ class DefaultMultiAccountStatusListProducerTest { val expected = listOf(accountStatusList1, accountStatusList2) Truth.assertThat(actual).containsExactly(expected) - coVerify(ordering = Ordering.SEQUENCE) { - accountsCRUDRepository.getUserWallets() + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets singleAccountStatusListSupplier( params = SingleAccountStatusListProducer.Params(userWalletId1), ) @@ -93,7 +97,7 @@ class DefaultMultiAccountStatusListProducerTest { fun `produce returns empty flow if userWallets is empty list`() = runTest { // Arrange val walletsFlow = MutableStateFlow>(emptyList()) - every { accountsCRUDRepository.getUserWallets() } returns walletsFlow + every { userWalletsListRepository.userWallets } returns walletsFlow // Act val actual = producer.produce().let(::getEmittedValues) @@ -101,28 +105,29 @@ class DefaultMultiAccountStatusListProducerTest { // Assert Truth.assertThat(actual).isEmpty() - coVerify(ordering = Ordering.SEQUENCE) { - accountsCRUDRepository.getUserWallets() + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets } } - // 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 = MutableStateFlow>(emptyList()) + every { userWalletsListRepository.userWallets } returns walletsFlow + + // Act + val actual = producer.produce().let(::getEmittedValues) + + // Assert + Truth.assertThat(actual).isEmpty() + + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets + } + } @Test fun `flow will updated if userWallets are updated`() = runTest { @@ -131,7 +136,7 @@ class DefaultMultiAccountStatusListProducerTest { val userWallet3 = mockk { every { walletId } returns userWalletId3 } val walletsFlow = MutableStateFlow(listOf(userWallet1, userWallet2)) - every { accountsCRUDRepository.getUserWallets() } returns walletsFlow + every { userWalletsListRepository.userWallets } returns walletsFlow val accountStatusList1 = mockk() val accountStatusList2 = mockk() @@ -170,8 +175,9 @@ class DefaultMultiAccountStatusListProducerTest { val expected2 = listOf(accountStatusList1, accountStatusList2, accountStatusList3) Truth.assertThat(actual2).containsExactly(expected2) - coVerify(ordering = Ordering.SEQUENCE) { - accountsCRUDRepository.getUserWallets() + coVerifySequence { + userWalletsListRepository.load() + userWalletsListRepository.userWallets singleAccountStatusListSupplier( params = SingleAccountStatusListProducer.Params(userWalletId1), ) @@ -187,7 +193,8 @@ class DefaultMultiAccountStatusListProducerTest { singleAccountStatusListSupplier( params = SingleAccountStatusListProducer.Params(userWalletId3), ) - accountsCRUDRepository.getUserWallets() + userWalletsListRepository.load() + userWalletsListRepository.userWallets singleAccountStatusListSupplier( params = SingleAccountStatusListProducer.Params(userWalletId1), ) diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt index c838e40466..af71ec2c02 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt @@ -65,7 +65,7 @@ internal class ApplyTokenListSortingUseCaseTest { @Test fun `when getAccountListSync throws exception then error should be received`() = runTest { // Arrange - val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = emptySet()) + val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = emptyList()) val exception = IllegalStateException("No internet connection") coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } throws exception @@ -92,7 +92,7 @@ internal class ApplyTokenListSortingUseCaseTest { @Test fun `when saveAccountsLocally throws exception then error should be received`() = runTest { // Arrange - val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = emptySet()) + val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = emptyList()) coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() @@ -114,14 +114,16 @@ internal class ApplyTokenListSortingUseCaseTest { coVerifyOrder { accountsCRUDRepository.getAccountListSync(userWalletId) accountsCRUDRepository.saveAccountsLocally(accountList) - accountsCRUDRepository.syncTokens(userWalletId = userWalletId) + } + coVerifyOrder(inverse = true) { + accountsCRUDRepository.syncTokens(userWalletId = any()) } } @Test fun `when syncTokens throws exception then error should be received`() = runTest { // Arrange - val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = emptySet()) + val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = emptyList()) coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() @@ -156,13 +158,13 @@ internal class ApplyTokenListSortingUseCaseTest { val accountList = AccountList.empty( userWalletId = userWalletId, - cryptoCurrencies = setOf(token1, token2, token3), + cryptoCurrencies = listOf(token1, token2, token3), sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ) coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() - val sortedTokens = setOf(token2, token3, token1) + val sortedTokens = listOf(token2, token3, token1) val updatedAccountList = AccountList.empty( userWalletId = userWalletId, cryptoCurrencies = sortedTokens, @@ -197,13 +199,13 @@ internal class ApplyTokenListSortingUseCaseTest { val accountList = AccountList.empty( userWalletId = userWalletId, - cryptoCurrencies = setOf(token1, token2, token3), + cryptoCurrencies = listOf(token1, token2, token3), sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ) coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() - val sortedTokens = setOf(token2, token3, token1) + val sortedTokens = listOf(token2, token3, token1) val updatedAccountList = AccountList.empty( userWalletId = userWalletId, cryptoCurrencies = sortedTokens, @@ -238,13 +240,13 @@ internal class ApplyTokenListSortingUseCaseTest { val accountList = AccountList.empty( userWalletId = userWalletId, - cryptoCurrencies = setOf(token1, token2, token3), + cryptoCurrencies = listOf(token1, token2, token3), sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ) coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() - val sortedTokens = setOf(token2, token3, token1) + val sortedTokens = listOf(token2, token3, token1) val updatedAccountList = AccountList.empty( userWalletId = userWalletId, cryptoCurrencies = sortedTokens, @@ -279,13 +281,13 @@ internal class ApplyTokenListSortingUseCaseTest { val accountList = AccountList.empty( userWalletId = userWalletId, - cryptoCurrencies = setOf(token1, token2, token3), + cryptoCurrencies = listOf(token1, token2, token3), sortType = TokensSortType.BALANCE, groupType = TokensGroupType.NETWORK, ) coEvery { accountsCRUDRepository.getAccountListSync(userWalletId) } returns accountList.some() - val sortedTokens = setOf(token2, token3, token1) + val sortedTokens = listOf(token2, token3, token1) val updatedAccountList = AccountList.empty( userWalletId = userWalletId, cryptoCurrencies = sortedTokens, @@ -323,13 +325,13 @@ internal class ApplyTokenListSortingUseCaseTest { name = "Account 1", icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), derivationIndex = 1, - cryptoCurrencies = setOf(token1, token2, token3), + cryptoCurrencies = listOf(token1, token2, token3), ) .getOrNull()!! val accountList = (AccountList.empty( userWalletId = userWalletId, - cryptoCurrencies = setOf(token1, token2, token3), + cryptoCurrencies = listOf(token1, token2, token3), sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ) + customAccount).getOrNull()!! @@ -344,7 +346,7 @@ internal class ApplyTokenListSortingUseCaseTest { val updatedAccountList = AccountList( userWalletId = userWalletId, accounts = listOf( - accountList.mainAccount.copy(cryptoCurrencies = setOf(token3, token2, token1)), + accountList.mainAccount.copy(cryptoCurrencies = listOf(token3, token2, token1)), customAccount, // unchanged due to error ), totalAccounts = accountList.totalAccounts, diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt index ba62fe222b..9471a7d81f 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt @@ -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), @@ -253,9 +252,9 @@ class GetAccountCurrencyByAddressUseCaseTest { network = currency.network, value = NetworkStatus.Unreachable(address = validNetworkAddress), ) - val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = setOf(currency)) + val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = listOf(currency)) - every { 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/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt index 3fc7c456cd..5c35e33427 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt @@ -103,7 +103,7 @@ class GetAccountCurrencyStatusUseCaseTest { val account = mockk(relaxed = true) { every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!! - every { this@mockk.cryptoCurrencies } returns setOf(currency) + every { this@mockk.cryptoCurrencies } returns listOf(currency) } val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) val accountStatus = AccountStatus.CryptoPortfolio( @@ -140,7 +140,7 @@ class GetAccountCurrencyStatusUseCaseTest { fun `invokeSync returns Some if network is null`() = runTest { // Arrange val account = mockk(relaxed = true) { - every { this@mockk.cryptoCurrencies } returns setOf(currency) + every { this@mockk.cryptoCurrencies } returns listOf(currency) } val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) val accountStatus = AccountStatus.CryptoPortfolio( @@ -223,7 +223,7 @@ class GetAccountCurrencyStatusUseCaseTest { val account = mockk(relaxed = true) { every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!! - every { this@mockk.cryptoCurrencies } returns setOf(currency) + every { this@mockk.cryptoCurrencies } returns listOf(currency) } val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) val accountStatus = AccountStatus.CryptoPortfolio( @@ -257,7 +257,7 @@ class GetAccountCurrencyStatusUseCaseTest { fun `invoke returns data if network is null`() = runTest { // Arrange val account = mockk(relaxed = true) { - every { this@mockk.cryptoCurrencies } returns setOf(currency) + every { this@mockk.cryptoCurrencies } returns listOf(currency) } val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) val accountStatus = AccountStatus.CryptoPortfolio( diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCaseTest.kt index 8bf3eb7e56..e55d32647c 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/RecoverCryptoPortfolioUseCaseTest.kt @@ -266,7 +266,7 @@ class RecoverCryptoPortfolioUseCaseTest { accountName = AccountName(name).getOrNull()!!, icon = icon, derivationIndex = derivationIndex, - cryptoCurrencies = emptySet(), + cryptoCurrencies = emptyList(), ) } diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt index f7a54bccda..bd97734029 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardProcessor.kt @@ -11,10 +11,12 @@ interface ScanCardProcessor { cardId: String? = null, allowsRequestAccessCodeFromRepository: Boolean = false, analyticsSource: AnalyticsParam.ScreensSources, + shouldCheckIsAlreadyActivated: Boolean, ): CompletionResult suspend fun scan( analyticsSource: AnalyticsParam.ScreensSources, + shouldCheckIsAlreadyActivated: Boolean, cardId: String? = null, onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {}, onWalletNotCreated: suspend () -> Unit = {}, diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt index 04abfb1fce..046e484ec6 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt @@ -49,9 +49,14 @@ class ScanCardUseCase( cardId: String?, allowRequestAccessCodeFromStorage: Boolean, ): Either { - return Either.catch { scanCardRepository.scanCard(cardId, allowRequestAccessCodeFromStorage) } - .mapLeft { e -> - e as? ScanCardException ?: ScanCardException.UnknownException(e) - } + return Either.catch { + scanCardRepository.scanCard( + cardId = cardId, + allowRequestAccessCodeFromStorage = allowRequestAccessCodeFromStorage, + shouldCheckIsAlreadyActivated = true, // TODO use correct when enable NEW_CARD_SCANNING_ENABLED + ) + }.mapLeft { e -> + e as? ScanCardException ?: ScanCardException.UnknownException(e) + } } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/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/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt index 90f2016383..0774f11004 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt @@ -17,5 +17,9 @@ interface ScanCardRepository { * @return a [ScanResponse] object with the result of the scan. * @throws [ScanCardException] if the scan process fails. */ - suspend fun scanCard(cardId: String?, allowRequestAccessCodeFromStorage: Boolean): ScanResponse + suspend fun scanCard( + cardId: String?, + allowRequestAccessCodeFromStorage: Boolean, + shouldCheckIsAlreadyActivated: Boolean, + ): ScanResponse } \ No newline at end of file diff --git a/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..c9f9aecb00 100644 --- a/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt +++ b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepository.kt @@ -123,6 +123,14 @@ interface UserWalletsListRepository { */ suspend fun clearPersistentData() + /** + * Transforms user wallets list according to the provided action. + * + * @param action The transformation action to apply. + * @throws IllegalArgumentException if the transformation action is invalid (e.g. reordering with missing or extra wallet IDs). + */ + suspend fun transform(action: UserWalletTransformAction) + /** * Checks if there are any secured wallets (wallets that are not locked with [LockMethod.NoLock]). */ diff --git a/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepositoryExt.kt b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepositoryExt.kt new file mode 100644 index 0000000000..c5ee21b86a --- /dev/null +++ b/domain/common/src/main/java/com/tangem/domain/common/wallets/UserWalletsListRepositoryExt.kt @@ -0,0 +1,52 @@ +package com.tangem.domain.common.wallets + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.domain.common.wallets.error.SaveWalletError +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +/** + * Update user wallet by [userWalletId] and return updated wallet. + * + * @param userWalletId The ID of the user wallet to update. + * @param transform A function that takes the current user wallet and returns the updated version. + * @return Either containing the updated user wallet on success or an error if the update fails. + */ +suspend fun UserWalletsListRepository.update( + userWalletId: UserWalletId, + transform: suspend (UserWallet) -> UserWallet, +): Either = either { + val userWallet = userWallets.value?.find { it.walletId == userWalletId } + requireNotNull(userWallet) { "Unable to find user wallet with provided ID: $userWalletId" } + + val updatedUserWallet = transform(userWallet) + + saveWithoutLock( + userWallet = updatedUserWallet, + canOverride = true, + ) + .bind() + + updatedUserWallet +} + +/** Get user wallet by [id] */ +fun UserWalletsListRepository.getSyncOrNull(id: UserWalletId): UserWallet? { + return userWallets.value?.find { it.walletId == id } +} + +/** Get user wallet by [id] or throw an exception if it is not found */ +fun UserWalletsListRepository.getSyncStrict(id: UserWalletId): UserWallet { + return requireNotNull(getSyncOrNull(id)) { "Unable to find user wallet with provided ID: $id" } +} + +/** Loads user wallets list and selected wallet and returns a flow of the list */ +fun UserWalletsListRepository.loadAndGet(): Flow> = flow { + load() + userWallets.collect { + emit(requireNotNull(it)) + } +} \ No newline at end of file diff --git a/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/EarnFilter.kt b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilter.kt new file mode 100644 index 0000000000..e09972f216 --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilter.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.earn.model + +import kotlinx.serialization.Serializable + +@Serializable +data class EarnFilter( + val earnFilterNetwork: EarnFilterNetwork, + val earnFilterType: EarnFilterType, +) \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilterNetwork.kt b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilterNetwork.kt new file mode 100644 index 0000000000..12de9b30ba --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilterNetwork.kt @@ -0,0 +1,27 @@ +package com.tangem.domain.earn.model + +import kotlinx.serialization.Serializable + +@Serializable +sealed interface EarnFilterNetwork { + + val isSelected: Boolean + + @Serializable + data class AllNetworks( + override val isSelected: Boolean, + ) : EarnFilterNetwork + + @Serializable + data class MyNetworks( + override val isSelected: Boolean, + ) : EarnFilterNetwork + + @Serializable + data class Specific( + override val isSelected: Boolean, + val id: String, + val symbol: String, + val fullName: String, + ) : EarnFilterNetwork +} \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilterType.kt b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilterType.kt new file mode 100644 index 0000000000..26c8760325 --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnFilterType.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.earn.model + +enum class EarnFilterType { + ALL, + STAKING, + YIELD, +} \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTokensListConfig.kt b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTokensListConfig.kt new file mode 100644 index 0000000000..20320ace05 --- /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 networks list of networkId. + * @param isForEarn flag to load mostly used tokens. + */ +@Serializable +data class EarnTokensListConfig( + val type: String?, + val networks: List?, + val isForEarn: Boolean, +) \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/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..7286e647c4 --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt @@ -0,0 +1,31 @@ +package com.tangem.domain.earn.repository + +import com.tangem.domain.earn.model.EarnFilter +import com.tangem.domain.earn.model.EarnTokensBatchFlow +import com.tangem.domain.earn.model.EarnTokensBatchingContext +import com.tangem.domain.models.earn.EarnNetworks +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() + + fun observeEarnNetworks(): Flow + + /** + * Fetch top N earn tokens by isForEarn = true and hold it in runtime store. + */ + suspend fun fetchTopEarnTokens(limit: Int) + + fun observeTopEarnTokens(): Flow + + fun observeEarnFilter(): Flow + + suspend fun setEarnFilter(filter: EarnFilter) +} \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchEarnNetworksUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchEarnNetworksUseCase.kt new file mode 100644 index 0000000000..aa426437e7 --- /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(): Either { + return Either.catch { + repository.fetchEarnNetworks() + } + } +} \ 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/GetEarnFilterUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnFilterUseCase.kt new file mode 100644 index 0000000000..0719f4b643 --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnFilterUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.earn.usecase + +import com.tangem.domain.earn.model.EarnFilter +import com.tangem.domain.earn.repository.EarnRepository +import kotlinx.coroutines.flow.Flow + +class GetEarnFilterUseCase(private val repository: EarnRepository) { + + operator fun invoke(): Flow { + return repository.observeEarnFilter() + } +} \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/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/earn/src/main/java/com/tangem/domain/earn/usecase/SetEarnFilterUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/SetEarnFilterUseCase.kt new file mode 100644 index 0000000000..3f666e4508 --- /dev/null +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/SetEarnFilterUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.earn.usecase + +import com.tangem.domain.earn.model.EarnFilter +import com.tangem.domain.earn.repository.EarnRepository + +class SetEarnFilterUseCase(private val repository: EarnRepository) { + + suspend operator fun invoke(filter: EarnFilter) { + repository.setEarnFilter(filter) + } +} \ No newline at end of file diff --git a/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProvider.kt b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProvider.kt index 49b5797e0d..ec1721ee8b 100644 --- a/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProvider.kt +++ b/domain/express/models/src/main/java/com/tangem/domain/express/models/ExpressProvider.kt @@ -17,6 +17,7 @@ import java.math.BigDecimal * @property isRecommended flag that indicates if this provider is recommended * @property slippage provider slippage * @property isExchangeOnlyWithinSingleAddress flag that indicates if exchange is only allowed within a single address + * @property isExtraIdSupported flag that indicates if provider supports transaction extras (memo, destination tag) * * Uses to store transaction data in datastore, when extends - should always add default value * to support backward compatibility @@ -43,4 +44,6 @@ data class ExpressProvider( val slippage: BigDecimal?, @Json(name = "exchangeOnlyWithinSingleAddress") val isExchangeOnlyWithinSingleAddress: Boolean = false, + @Json(name = "isExtraIdSupported") + val isExtraIdSupported: Boolean = false, ) \ No newline at end of file diff --git a/domain/hot-wallet/build.gradle.kts b/domain/hot-wallet/build.gradle.kts index 471afb3452..621b607241 100644 --- a/domain/hot-wallet/build.gradle.kts +++ b/domain/hot-wallet/build.gradle.kts @@ -14,4 +14,10 @@ dependencies { implementation(projects.domain.wallets.models) implementation(deps.kotlin.coroutines) + implementation(deps.arrow.core) + + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) + testImplementation(deps.test.mockk) } \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt new file mode 100644 index 0000000000..28f7be6bd3 --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt @@ -0,0 +1,61 @@ +package com.tangem.domain.hotwallet + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import java.util.concurrent.TimeUnit + +class CheckHotWalletUpgradeBannerUseCase( + private val hotWalletRepository: HotWalletRepository, +) { + suspend operator fun invoke( + walletId: UserWalletId, + hasBalance: Boolean, + shouldShowUpgradeBanner: Boolean, + closureTimestamp: Long?, + ): Either = try { + val currentTime = System.currentTimeMillis() + val creationTimestamp = hotWalletRepository.getWalletCreationTimestamp(walletId) + + val creationTimestampActual = if (creationTimestamp == null) { + // If creationTimestamp is null (wallet was created before this feature was released), + // store the current timestamp and use it below + hotWalletRepository.setWalletCreationTimestamp(walletId, currentTime) + currentTime + } else { + creationTimestamp + } + + val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(walletId) + + val daysSinceCreation = TimeUnit.MILLISECONDS.toDays(currentTime - creationTimestampActual) + val daysSinceClosure = closureTimestamp?.let { TimeUnit.MILLISECONDS.toDays(currentTime - it) } + + // Wallet balance is positive, but the first top-up hasn't been tracked yet + if (hasBalance && !hasHadFirstTopUp) { + hotWalletRepository.setHasHadFirstTopUp(walletId, true) + hotWalletRepository.setShouldShowUpgradeBanner(walletId, true) + hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, null) + hotWalletRepository.markFirstTopUpDetectedThisSession(walletId) + } + + val shouldShow = when { + // Banner should be shown (e.g., because of the first top-up in the previous session) + shouldShowUpgradeBanner -> !hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) + // Banner was closed; it happened more than BANNER_RESHOW_DAYS (30) days ago + closureTimestamp != null && daysSinceClosure != null && daysSinceClosure >= BANNER_RESHOW_DAYS -> true + // Banner hasn't been closed; wallet was created more than BANNER_RESHOW_DAYS (30) days ago + closureTimestamp == null && daysSinceCreation >= BANNER_RESHOW_DAYS -> true + else -> false + } + shouldShow.right() + } catch (e: Exception) { + e.left() + } + + companion object { + const val BANNER_RESHOW_DAYS = 30L + } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCase.kt new file mode 100644 index 0000000000..17591a2bb1 --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCase.kt @@ -0,0 +1,20 @@ +package com.tangem.domain.hotwallet + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId + +class CloseHotWalletUpgradeBannerUseCase( + private val hotWalletRepository: HotWalletRepository, +) { + suspend operator fun invoke(walletId: UserWalletId): Either = try { + val currentTime = System.currentTimeMillis() + hotWalletRepository.setShouldShowUpgradeBanner(walletId, false) + hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, currentTime) + Unit.right() + } catch (e: Exception) { + e.left() + } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/GetUpgradeBannerClosureTimestampUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/GetUpgradeBannerClosureTimestampUseCase.kt new file mode 100644 index 0000000000..dbe9d2f2af --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/GetUpgradeBannerClosureTimestampUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.hotwallet + +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow + +class GetUpgradeBannerClosureTimestampUseCase( + private val hotWalletRepository: HotWalletRepository, +) { + operator fun invoke(userWalletId: UserWalletId): Flow { + return hotWalletRepository.upgradeBannerClosureTimestamp(userWalletId) + } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/ShouldShowUpgradeHotWalletBannerUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/ShouldShowUpgradeHotWalletBannerUseCase.kt new file mode 100644 index 0000000000..49e17e6c7c --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/ShouldShowUpgradeHotWalletBannerUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.domain.hotwallet + +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow + +class ShouldShowUpgradeHotWalletBannerUseCase( + private val hotWalletRepository: HotWalletRepository, +) { + operator fun invoke(userWalletId: UserWalletId): Flow = + hotWalletRepository.shouldShowUpgradeBanner(userWalletId) +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt index e228cd4419..2d63b01cde 100644 --- a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt @@ -12,4 +12,24 @@ interface HotWalletRepository { fun accessCodeSkipped(userWalletId: UserWalletId): Flow suspend fun setAccessCodeSkipped(userWalletId: UserWalletId, skipped: Boolean) + + fun shouldShowUpgradeBanner(userWalletId: UserWalletId): Flow + + suspend fun setShouldShowUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) + + fun upgradeBannerClosureTimestamp(userWalletId: UserWalletId): Flow + + suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long?) + + suspend fun getWalletCreationTimestamp(userWalletId: UserWalletId): Long? + + suspend fun setWalletCreationTimestamp(userWalletId: UserWalletId, timestamp: Long) + + suspend fun hasHadFirstTopUp(userWalletId: UserWalletId): Boolean + + suspend fun setHasHadFirstTopUp(userWalletId: UserWalletId, hasTopUp: Boolean) + + fun isFirstTopUpDetectedThisSession(userWalletId: UserWalletId): Boolean + + fun markFirstTopUpDetectedThisSession(userWalletId: UserWalletId) } \ No newline at end of file diff --git a/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt new file mode 100644 index 0000000000..447864f896 --- /dev/null +++ b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt @@ -0,0 +1,259 @@ +package com.tangem.domain.hotwallet + +import arrow.core.Either +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.util.concurrent.TimeUnit + +class CheckHotWalletUpgradeBannerUseCaseTest { + + private val hotWalletRepository: HotWalletRepository = mockk(relaxed = true) + private val useCase = CheckHotWalletUpgradeBannerUseCase(hotWalletRepository) + + private val walletId = UserWalletId("0123456789ABCDEF") + + @Test + fun `GIVEN creation timestamp is null WHEN invoke THEN set timestamp and return false`() = runTest { + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns null + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + closureTimestamp = null, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + coVerify { hotWalletRepository.setWalletCreationTimestamp(walletId, any()) } + } + + @Test + fun `GIVEN shouldShowUpgradeBanner is true and hasBalance WHEN invoke THEN return true`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = true, + closureTimestamp = null, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isTrue() + } + + @Test + fun `GIVEN shouldShowUpgradeBanner is true WHEN invoke THEN return true regardless of balance`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = true, + closureTimestamp = null, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isTrue() + } + + @Test + fun `GIVEN closure timestamp exists and 30 days since closure WHEN invoke THEN return true`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) + val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = false, + closureTimestamp = closureTimestamp, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isTrue() + } + + @Test + fun `GIVEN closure timestamp exists but less than 30 days since closure WHEN invoke THEN return false`() = + runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) + val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(15) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = false, + closureTimestamp = closureTimestamp, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + } + + @Test + fun `GIVEN no flags set and no closure and 30 days since creation WHEN invoke THEN return true`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + closureTimestamp = null, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isTrue() + } + + @Test + fun `GIVEN no flags set but less than 30 days since creation WHEN invoke THEN return false`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(15) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + closureTimestamp = null, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + } + + @Test + fun `GIVEN no flags set but closure timestamp exists WHEN invoke THEN return false`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) + val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + closureTimestamp = closureTimestamp, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + } + + @Test + fun `GIVEN first top-up detected WHEN invoke THEN return false and mark session`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = false, + closureTimestamp = null, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + coVerify { hotWalletRepository.setHasHadFirstTopUp(walletId, true) } + coVerify { hotWalletRepository.setShouldShowUpgradeBanner(walletId, true) } + coVerify { hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, null) } + verify { hotWalletRepository.markFirstTopUpDetectedThisSession(walletId) } + } + + @Test + fun `GIVEN first top-up detected this session WHEN invoke THEN return false`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns true + + val result = useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = true, + closureTimestamp = null, + ) + + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isFalse() + } + + @Test + fun `GIVEN already had first top-up WHEN invoke with balance THEN do not set flags again`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + useCase( + walletId = walletId, + hasBalance = true, + shouldShowUpgradeBanner = true, + closureTimestamp = null, + ) + + coVerify(exactly = 0) { hotWalletRepository.setHasHadFirstTopUp(any(), any()) } + coVerify(exactly = 0) { hotWalletRepository.setShouldShowUpgradeBanner(any(), any()) } + } + + @Test + fun `GIVEN multiple re-emissions with same state WHEN invoke THEN return same result`() = runTest { + val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) + coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp + coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false + every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false + + val result1 = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + closureTimestamp = null, + ) + val result2 = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + closureTimestamp = null, + ) + val result3 = useCase( + walletId = walletId, + hasBalance = false, + shouldShowUpgradeBanner = false, + closureTimestamp = null, + ) + + assertThat((result1 as Either.Right).value).isTrue() + assertThat((result2 as Either.Right).value).isTrue() + assertThat((result3 as Either.Right).value).isTrue() + } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCaseTest.kt b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCaseTest.kt new file mode 100644 index 0000000000..d961d07617 --- /dev/null +++ b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CloseHotWalletUpgradeBannerUseCaseTest.kt @@ -0,0 +1,39 @@ +package com.tangem.domain.hotwallet + +import arrow.core.Either +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class CloseHotWalletUpgradeBannerUseCaseTest { + + private val hotWalletRepository: HotWalletRepository = mockk(relaxed = true) + private val useCase = CloseHotWalletUpgradeBannerUseCase(hotWalletRepository) + + private val walletId = UserWalletId("0123456789ABCDEF") + + @Test + fun `WHEN invoke THEN set banner flag to false and closure timestamp`() = runTest { + val result = useCase(walletId) + + assertThat(result).isInstanceOf(Either.Right::class.java) + coVerify { hotWalletRepository.setShouldShowUpgradeBanner(walletId, false) } + coVerify { hotWalletRepository.setUpgradeBannerClosureTimestamp(walletId, any()) } + } + + @Test + fun `GIVEN repository throws exception WHEN invoke THEN return Either Left`() = runTest { + val exception = RuntimeException("Test error") + coEvery { hotWalletRepository.setShouldShowUpgradeBanner(walletId, false) } throws exception + + val result = useCase(walletId) + + assertThat(result).isInstanceOf(Either.Left::class.java) + assertThat((result as Either.Left).value).isEqualTo(exception) + } +} \ No newline at end of file diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt index 3deeee528d..5d6463b790 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarket.kt @@ -14,9 +14,16 @@ data class TokenMarket( val tokenCharts: Charts, val yieldRate: BigDecimal?, val updateTimestamp: Long?, + val networks: List?, private val imageHost: String, ) { + data class Network( + val networkId: String, + val contractAddress: String?, + val decimalCount: Int?, + ) + data class Charts( val h24: TokenChart?, val week: TokenChart?, diff --git a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt index ffd82b8909..c1a1d9631f 100644 --- a/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt +++ b/domain/markets/models/src/main/kotlin/com/tangem/domain/markets/TokenMarketListConfig.kt @@ -5,6 +5,7 @@ data class TokenMarketListConfig( val searchText: String?, val priceChangeInterval: Interval, val order: Order, + val shouldNetworks: Boolean? = null, ) { enum class Order { diff --git a/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/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt index 851c25e463..a0442452ef 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -42,7 +42,7 @@ sealed interface Account { override val accountName: AccountName, val icon: CryptoPortfolioIcon, val derivationIndex: DerivationIndex, - val cryptoCurrencies: Set, + val cryptoCurrencies: List, ) : Account { /** Indicates if the account is the main account */ @@ -60,7 +60,7 @@ sealed interface Account { fun copy( accountName: AccountName = this.accountName, icon: CryptoPortfolioIcon = this.icon, - cryptoCurrencies: Set = this.cryptoCurrencies, + cryptoCurrencies: List = this.cryptoCurrencies, ): CryptoPortfolio { return CryptoPortfolio( accountId = this.accountId, @@ -102,14 +102,14 @@ sealed interface Account { name: String, icon: CryptoPortfolioIcon, derivationIndex: Int, - cryptoCurrencies: Set = emptySet(), + cryptoCurrencies: List = emptyList(), ): Either { return either { val accountName = AccountName(value = name).getOrElse { raise(AccountNameError(cause = it)) } - val derivationIndex = DerivationIndex(value = derivationIndex).getOrElse { + val index = DerivationIndex(value = derivationIndex).getOrElse { raise(DerivationIndexError(cause = it)) } @@ -117,7 +117,7 @@ sealed interface Account { accountId = accountId, accountName = accountName, icon = icon, - derivationIndex = derivationIndex, + derivationIndex = index, cryptoCurrencies = cryptoCurrencies, ) } @@ -138,7 +138,7 @@ sealed interface Account { accountName: AccountName, icon: CryptoPortfolioIcon, derivationIndex: DerivationIndex, - cryptoCurrencies: Set = emptySet(), + cryptoCurrencies: List = emptyList(), ): CryptoPortfolio { return CryptoPortfolio( accountId = accountId, @@ -157,7 +157,7 @@ sealed interface Account { */ fun createMainAccount( userWalletId: UserWalletId, - cryptoCurrencies: Set = emptySet(), + cryptoCurrencies: List = emptyList(), ): CryptoPortfolio { val derivationIndex = DerivationIndex.Main @@ -175,11 +175,12 @@ sealed interface Account { } } - class Payment : Account { - override val accountId: AccountId - get() = TODO("Not yet implemented") - override val accountName: AccountName - get() = TODO("Not yet implemented") + @Serializable + data class Payment( + override val accountId: AccountId, + override val accountName: AccountName, + val cryptoCurrencies: List, + ) : Account { init { error("Not yet implemented") @@ -190,5 +191,5 @@ sealed interface Account { val Account.derivationIndex: DerivationIndex? get() = when (this) { is Account.CryptoPortfolio -> derivationIndex - is Account.Payment -> TODO("[REDACTED_JIRA]") + is Account.Payment -> null } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt index bffd56a6c4..0c65375595 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountId.kt @@ -71,5 +71,9 @@ data class AccountId private constructor( return AccountId(value = value, userWalletId = userWalletId) } + + fun forPaymentAccount(userWalletId: UserWalletId): AccountId { + return AccountId(value = "payment_$userWalletId", userWalletId = userWalletId) + } } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt index b04e872b58..31df2e7281 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/AccountStatus.kt @@ -1,6 +1,7 @@ package com.tangem.domain.models.account import com.tangem.domain.core.lce.Lce +import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.quote.PriceChange import com.tangem.domain.models.tokenlist.TokenList @@ -33,13 +34,23 @@ sealed interface AccountStatus { override val account: Account.CryptoPortfolio, val tokenList: TokenList, val priceChangeLce: Lce, + ) : AccountStatus { + fun flattenCurrencies(): List { + return tokenList.flattenCurrencies() + } + } + + @Serializable + data class Payment( + override val account: Account.Payment, + val totalFiatBalance: TotalFiatBalance, ) : AccountStatus +} - fun flattenCurrencies(): List = when (this) { - is CryptoPortfolio -> tokenList.flattenCurrencies() - } +fun Iterable.filterCryptoPortfolio(): List { + return filterIsInstance() +} - fun getCryptoTokenList(): TokenList = when (this) { - is CryptoPortfolio -> tokenList - } +fun Sequence.filterCryptoPortfolio(): Sequence { + return filterIsInstance() } \ No newline at end of file diff --git a/domain/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..114bbad106 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetwork.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.models.earn + +import kotlinx.serialization.Serializable + +/** + * Model for networks in Earn. + * @param networkId - id of network + * @param fullName - full name of network + * @param symbol - symbol of network + * @param isAdded - is exists in any user's wallet + */ +@Serializable +data class EarnNetwork( + val networkId: String, + val fullName: String, + val symbol: String, + val isAdded: Boolean, +) \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/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..e3fc112731 --- /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/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt index 9578f81fb1..0695ba92c3 100644 --- a/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt +++ b/domain/models/src/test/kotlin/com/tangem/domain/models/account/AccountTest.kt @@ -50,8 +50,8 @@ class AccountTest { @Test fun `CryptoPortfolio tokensCount`() { // Arrange - val emptyCurrencies = emptySet() - val filledCurrencies = setOf(mockk()) + val emptyCurrencies = emptyList() + val filledCurrencies = listOf(mockk()) // Act val actual1 = createCryptoPortfolioStub(currencies = emptyCurrencies) @@ -68,8 +68,8 @@ class AccountTest { @Test fun `CryptoPortfolio networksCount`() { // Arrange - val emptyCurrencies = emptySet() - val filledCurrencies = setOf( + val emptyCurrencies = emptyList() + val filledCurrencies = listOf( mockk { every { network } returns mockk() }, @@ -102,7 +102,7 @@ class AccountTest { name = name, icon = mockk(), derivationIndex = 0, - cryptoCurrencies = emptySet(), + cryptoCurrencies = emptyList(), ) .leftOrNull()!! @@ -123,7 +123,7 @@ class AccountTest { name = "Test Account", icon = CryptoPortfolioIcon.ofMainAccount(userWalletId = UserWalletId("011")), derivationIndex = derivationIndex.value, - cryptoCurrencies = emptySet(), + cryptoCurrencies = emptyList(), ) .getOrNull()!! @@ -150,7 +150,7 @@ class AccountTest { accountName = AccountName.DefaultMain, icon = CryptoPortfolioIcon.ofMainAccount(userWalletId), derivationIndex = derivationIndex, - cryptoCurrencies = emptySet(), + cryptoCurrencies = emptyList(), ) Truth.assertThat(actual).isEqualTo(expected) @@ -161,7 +161,7 @@ class AccountTest { userWalletId: UserWalletId = UserWalletId("011"), name: String = "Test Account", derivationIndex: Int = 0, - currencies: Set = emptySet(), + currencies: List = emptyList(), ): CryptoPortfolio { val accountIndex = DerivationIndex(value = derivationIndex).getOrNull()!! diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt index 53f7b08f65..e6024ff337 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/PushNotificationsRepository.kt @@ -1,6 +1,7 @@ package com.tangem.domain.notifications.repository import arrow.core.Either +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.notifications.models.ApplicationId import com.tangem.domain.notifications.models.NotificationsEligibleNetwork import com.tangem.domain.notifications.models.NotificationsError @@ -20,4 +21,6 @@ interface PushNotificationsRepository { @Throws suspend fun getEligibleNetworks(): List + + suspend fun isNotificationsEnabled(userWalletId: UserWalletId): Boolean } \ No newline at end of file diff --git a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt index 89edac21b3..5c8e52d327 100644 --- a/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt +++ b/domain/swap/models/src/main/java/com/tangem/domain/swap/models/SwapTransactionListModel.kt @@ -14,8 +14,8 @@ data class SwapTransactionListModel( val toCryptoCurrencyId: String, val fromCryptoCurrency: CryptoCurrency, val toCryptoCurrency: CryptoCurrency, - val fromAccount: Account.CryptoPortfolio?, - val toAccount: Account.CryptoPortfolio?, + val fromAccount: Account?, + val toAccount: Account?, val transactions: List, ) diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt index 8f26ccadb6..09bbb443fe 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapRepositoryV2.kt @@ -83,6 +83,7 @@ interface SwapRepositoryV2 { toCryptoCurrency: CryptoCurrency, fromAmount: String, toAddress: String, + toExtraId: String?, expressProvider: ExpressProvider, rateType: ExpressRateType, expressOperationType: ExpressOperationType, diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt b/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt index 7fe9292ea2..3e22c172a7 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/SwapTransactionRepository.kt @@ -30,8 +30,8 @@ interface SwapTransactionRepository { userWalletId: UserWalletId, fromCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, transaction: SwapTransactionModel, ) diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt index 0f0638c9a8..fed0e324ab 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapDataUseCase.kt @@ -24,6 +24,7 @@ class GetSwapDataUseCase( fromAmount: String, toCryptoCurrency: CryptoCurrency, toAddress: String, + toExtraId: String?, expressProvider: ExpressProvider, rateType: ExpressRateType, expressOperationType: ExpressOperationType, @@ -34,6 +35,7 @@ class GetSwapDataUseCase( fromAmount = fromAmount, toCryptoCurrency = toCryptoCurrency, toAddress = toAddress, + toExtraId = toExtraId, expressProvider = expressProvider, rateType = rateType, expressOperationType = expressOperationType, diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt index af3d4fe6aa..fa54d42d79 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/GetSwapSupportedPairsUseCase.kt @@ -69,12 +69,20 @@ class GetSwapSupportedPairsUseCase( .filter { pair -> // Search available to swap currency cryptoCurrencyList.any { currencyStatus -> - // Allowed only on networks without tx extras (e.i. memo and destination tag) - val isExtrasSupported = currencyStatus.network.transactionExtrasType.isTxExtrasSupported() - currencyStatus.id == pair.to.currency.id && !isExtrasSupported + currencyStatus.id == pair.to.currency.id } } - .map { pair -> SwapCryptoCurrency(groupingCurrency(pair), pair.providers) } + .map { pair -> + val toCurrency = groupingCurrency(pair) + val isTxExtrasSupported = toCurrency.currency.network.transactionExtrasType.isTxExtrasSupported() + val filteredProviders = if (isTxExtrasSupported) { + pair.providers.filter { it.isExtraIdSupported } + } else { + pair.providers + } + SwapCryptoCurrency(toCurrency, filteredProviders) + } + .filter { it.providers.isNotEmpty() } .toList() val unavailableCryptoCurrencies = diff --git a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt index 92fcc325e3..70c9811d57 100644 --- a/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt +++ b/domain/swap/src/main/java/com/tangem/domain/swap/usecase/SwapTransactionSentUseCase.kt @@ -22,8 +22,8 @@ class SwapTransactionSentUseCase( userWallet: UserWallet, fromCryptoCurrencyStatus: CryptoCurrencyStatus, toCryptoCurrencyStatus: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, swapDataTransactionModel: SwapDataTransactionModel, provider: ExpressProvider, txHash: String, diff --git a/domain/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/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index 8488e0c17b..9bf72aeddb 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(projects.domain.transaction.models) implementation(projects.domain.demo) implementation(projects.domain.card) + implementation(projects.domain.notifications) api(projects.domain.networks) testRuntimeOnly(deps.test.junit5.engine) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt index efa2d59016..74958179f4 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/TransactionRepository.kt @@ -125,5 +125,5 @@ interface TransactionRepository { network: Network, ): com.tangem.blockchain.extensions.Result> - suspend fun sendTransactionHash(hash: String, transactionType: EventTransactionTypeDto) + suspend fun sendTransactionHash(hash: String, transactionType: EventTransactionTypeDto, userAddress: String?) } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt index f566a5a93b..e43d756df6 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/WalletAddressServiceRepository.kt @@ -23,7 +23,7 @@ interface WalletAddressServiceRepository { suspend fun validateAddress(userWalletId: UserWalletId, network: Network, address: String): Boolean - fun validateMemo(network: Network, memo: String): Boolean + suspend fun validateMemo(userWalletId: UserWalletId, network: Network, memo: String): Boolean suspend fun parseSharedAddress(input: String, network: Network): ParsedQrCode } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 17b9fc38cc..05bc7a7445 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -25,6 +25,7 @@ import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.networks.single.SingleNetworkStatusFetcher +import com.tangem.domain.notifications.repository.PushNotificationsRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.error.parseWrappedError @@ -44,6 +45,7 @@ class SendTransactionUseCase( private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, private val parallelUpdatingScope: CoroutineScope, private val getHotWalletSigner: (UserWallet.Hot) -> TransactionSigner, + private val pushNotificationsRepository: PushNotificationsRepository, ) { suspend operator fun invoke( txsData: List, @@ -112,7 +114,11 @@ class SendTransactionUseCase( } .fold( ifRight = { result -> - processSentTransactionsHashes(txsData, result.hashes) + processSentTransactionsHashes( + userWallet = userWallet, + transactions = txsData, + hashes = result.hashes, + ) result.hashes.right() }, ifLeft = { it.left() }, @@ -128,11 +134,19 @@ class SendTransactionUseCase( .map { it.first() } } - private fun processSentTransactionsHashes(transactions: List, hashes: List) { + private fun processSentTransactionsHashes( + userWallet: UserWallet, + transactions: List, + hashes: List, + ) { parallelUpdatingScope.launch { withContext(NonCancellable) { transactions.forEachIndexed { ind, tx -> - sendHashToBackendIfNeeded(tx, hashes[ind]) + sendHashToBackendIfNeeded( + userWallet = userWallet, + transaction = tx, + txHash = hashes[ind], + ) } } } @@ -141,7 +155,11 @@ class SendTransactionUseCase( /** * Sends tx hash to backend for specific transaction types */ - private suspend fun sendHashToBackendIfNeeded(transaction: TransactionData, txHash: String) { + private suspend fun sendHashToBackendIfNeeded( + userWallet: UserWallet, + transaction: TransactionData, + txHash: String, + ) { (transaction as? TransactionData.Uncompiled)?.let { tx -> val extras = tx.extras when (extras) { @@ -152,7 +170,19 @@ class SendTransactionUseCase( is EthereumYieldSupplyExitCallData -> EventTransactionTypeDto.WITHDRAW else -> return } - transactionRepository.sendTransactionHash(txHash, txType) + val isPushNotificationEnabled = pushNotificationsRepository.isNotificationsEnabled( + userWallet.walletId, + ) + val userAddress = if (isPushNotificationEnabled) { + transaction.sourceAddress + } else { + null + } + transactionRepository.sendTransactionHash( + hash = txHash, + transactionType = txType, + userAddress = userAddress, + ) } } } diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt index 364eef4916..eb69e7feff 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ValidateWalletMemoUseCase.kt @@ -3,7 +3,8 @@ package com.tangem.domain.transaction.usecase import arrow.core.Either import arrow.core.left import arrow.core.right -import com.tangem.domain.models.network.Network +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.transaction.error.ValidateMemoError @@ -14,9 +15,17 @@ class ValidateWalletMemoUseCase( private val walletAddressServiceRepository: WalletAddressServiceRepository, ) { - operator fun invoke(network: Network, memo: String): Either { + suspend operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + memo: String, + ): Either { return try { - val isValidMemo = walletAddressServiceRepository.validateMemo(network, memo) + val isValidMemo = walletAddressServiceRepository.validateMemo( + userWalletId = userWalletId, + network = cryptoCurrency.network, + memo = memo, + ) if (isValidMemo) { Unit.right() } else { diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index 3dfad027a7..559774d3e2 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(projects.domain.core) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) + implementation(projects.features.swap.domain) /** Feature API - remove after removing [TangemPayFeatureToggles] */ implementation(projects.features.tangempay.details.api) diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayWithdrawState.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayWithdrawState.kt new file mode 100644 index 0000000000..5d002e6969 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayWithdrawState.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.pay + +data class TangemPayWithdrawState( + val orderId: String, + val exchangeData: TangemPayWithdrawExchangeState?, + val txHash: String? = null, +) + +data class TangemPayWithdrawExchangeState( + val txId: String, + val fromNetwork: String, + val fromAddress: String, + val payInAddress: String, + val payInExtraId: String?, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderData.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderData.kt new file mode 100644 index 0000000000..42b7001564 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderData.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.pay.model + +data class OrderData( + val status: OrderStatus, + val withdrawTxHash: String?, +) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt index 9ae1fd414b..09b25edc3a 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/CustomerOrderRepository.kt @@ -2,12 +2,10 @@ package com.tangem.domain.pay.repository import arrow.core.Either import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.model.OrderData import com.tangem.domain.visa.error.VisaApiError interface CustomerOrderRepository { - suspend fun getOrderStatus(userWalletId: UserWalletId, orderId: String): Either - - suspend fun hasWithdrawOrder(userWalletId: UserWalletId): Boolean + suspend fun getOrderData(userWalletId: UserWalletId, orderId: String): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt similarity index 64% rename from domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt rename to domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt index fed66aab7d..6079a4e874 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPaySwapRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt @@ -4,15 +4,21 @@ import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.TangemPayWithdrawExchangeState import com.tangem.domain.pay.WithdrawalResult import java.math.BigDecimal -interface TangemPaySwapRepository { +interface TangemPayWithdrawRepository { suspend fun withdraw( userWallet: UserWallet, receiverAddress: String, cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, + exchangeData: TangemPayWithdrawExchangeState, ): Either + + suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean + + suspend fun pollWithdrawOrdersIfNeeds(userWallet: UserWallet) } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 7ff408d1c1..647e6ad412 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -137,13 +137,13 @@ class TangemPayMainScreenCustomerInfoUseCase( userWalletId: UserWalletId, orderId: String, ): Either { - return customerOrderRepository.getOrderStatus(userWalletId, orderId = orderId) + return customerOrderRepository.getOrderData(userWalletId, orderId = orderId) .fold( ifLeft = { error -> error.mapErrorForCustomer().left() }, - ifRight = { orderStatus -> - when (orderStatus) { + ifRight = { orderData -> + when (orderData.status) { // Kyc is passed and user waits for order creation -> no need to get customer info OrderStatus.NEW, OrderStatus.PROCESSING, @@ -154,7 +154,7 @@ class TangemPayMainScreenCustomerInfoUseCase( kycStatus = CustomerInfo.KycStatus.APPROVED, cardInfo = null, ), - orderStatus = orderStatus, + orderStatus = orderData.status, ).right() // Order was created/cancelled -> clear order id and get customer info @@ -164,11 +164,11 @@ class TangemPayMainScreenCustomerInfoUseCase( -> { onboardingRepository.clearOrderId(userWalletId) // If order was cancelled -> start order creation - if (orderStatus == OrderStatus.CANCELED) onboardingRepository.createOrder(userWalletId) + if (orderData.status == OrderStatus.CANCELED) onboardingRepository.createOrder(userWalletId) onboardingRepository.getCustomerInfo(userWalletId = userWalletId) .mapLeft { it.mapErrorForCustomer() } .map { customerInfo -> - MainScreenCustomerInfo(info = customerInfo, orderStatus = orderStatus) + MainScreenCustomerInfo(info = customerInfo, orderStatus = orderData.status) } } } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt index e2abfd227f..d98264599f 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayWithdrawUseCase.kt @@ -4,6 +4,7 @@ import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.pay.TangemPayWithdrawExchangeState import com.tangem.domain.pay.WithdrawalResult import java.math.BigDecimal @@ -14,5 +15,6 @@ interface TangemPayWithdrawUseCase { cryptoAmount: BigDecimal, cryptoCurrencyId: CryptoCurrency.RawID, receiverCexAddress: String, + exchangeData: TangemPayWithdrawExchangeState, ): Either } \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt index 49ae45531a..cf3f4c9f37 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt @@ -8,7 +8,7 @@ import com.tangem.domain.models.wallet.UserWallet data class WcSession( val wallet: UserWallet, - val account: Account?, + val account: Account.CryptoPortfolio?, val networks: Set, val sdkModel: WcSdkSession, val securityStatus: CheckDAppResult, diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index b7b3798d96..f11102091a 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(projects.domain.walletManager) implementation(projects.libs.blockchainSdk) implementation(projects.libs.tangemSdkApi) + implementation(projects.domain.account) implementation(projects.domain.models) implementation(projects.domain.tokens) implementation(projects.domain.card) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt index be4e913124..850782d36a 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/analytics/WalletSettingsAnalyticEvents.kt @@ -171,6 +171,10 @@ sealed class WalletSettingsAnalyticEvents( event = "Wallet Upgraded", ), AppsFlyerIncludedEvent + class WalletsReorder : WalletSettingsAnalyticEvents( + event = "Longtap - Wallets Order", + ) + enum class RecoveryPhraseScreenAction(val value: String) { Upgrade("Upgrade"), Backup("Backup"), diff --git a/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/GenerateBuyTangemCardLinkUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt index b146be18d6..a9ef754830 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GenerateBuyTangemCardLinkUseCase.kt @@ -2,7 +2,7 @@ package com.tangem.domain.wallets.usecase import com.tangem.common.TangemSiteUrlBuilder import com.tangem.domain.wallets.repository.WalletsPromoRepository -import java.util.Locale +import com.tangem.utils.SupportedLanguages class GenerateBuyTangemCardLinkUseCase( private val walletsPromoRepository: WalletsPromoRepository, @@ -15,7 +15,7 @@ class GenerateBuyTangemCardLinkUseCase( suspend operator fun invoke(utmCampaign: String?): String { val refCode = walletsPromoRepository.getReferralCodeIfExists() val refCodeTag = TangemSiteUrlBuilder.getRefCodeTag(refCode) - val langCode = Locale.getDefault().language + val langCode = SupportedLanguages.getCurrentSupportedLanguageCode() val utmTags = TangemSiteUrlBuilder.getUtmTags(utmCampaign) return "https://buy.tangem.com/$langCode?$utmTags&$refCodeTag" } 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/SetNotificationsEnabledUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt index 5a66ceb5ab..c749d56049 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt @@ -1,13 +1,13 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either -import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.repository.WalletsRepository class SetNotificationsEnabledUseCase( private val walletsRepository: WalletsRepository, - private val currenciesRepository: CurrenciesRepository, + private val accountsCRUDRepository: AccountsCRUDRepository, ) { suspend operator fun invoke(userWalletId: UserWalletId, isEnabled: Boolean): Either = @@ -16,7 +16,7 @@ class SetNotificationsEnabledUseCase( userWalletId = userWalletId, isEnabled = isEnabled, ) - currenciesRepository.syncTokens(userWalletId) + accountsCRUDRepository.syncTokens(userWalletId) }.onLeft { walletsRepository.setNotificationsEnabled( userWalletId = userWalletId, diff --git a/domain/wallets/src/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/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCaseTest.kt b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCaseTest.kt new file mode 100644 index 0000000000..73d291ba22 --- /dev/null +++ b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCaseTest.kt @@ -0,0 +1,131 @@ +package com.tangem.domain.wallets.usecase + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifyOrder +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class SetNotificationsEnabledUseCaseTest { + + private lateinit var useCase: SetNotificationsEnabledUseCase + private lateinit var walletsRepository: WalletsRepository + private lateinit var accountsCRUDRepository: AccountsCRUDRepository + + @Before + fun setup() { + walletsRepository = mockk() + accountsCRUDRepository = mockk() + useCase = SetNotificationsEnabledUseCase( + walletsRepository = walletsRepository, + accountsCRUDRepository = accountsCRUDRepository, + ) + } + + @Test + fun `GIVEN notifications enabled successfully WHEN invoke THEN return Right with Unit`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = true + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } just runs + coEvery { accountsCRUDRepository.syncTokens(userWalletId) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isRight()).isTrue() + coVerifyOrder { + walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) + accountsCRUDRepository.syncTokens(userWalletId) + } + } + + @Test + fun `GIVEN notifications disabled successfully WHEN invoke THEN return Right with Unit`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = false + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } just runs + coEvery { accountsCRUDRepository.syncTokens(userWalletId) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isRight()).isTrue() + coVerifyOrder { + walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) + accountsCRUDRepository.syncTokens(userWalletId) + } + } + + @Test + fun `GIVEN setNotificationsEnabled throws exception WHEN invoke THEN return Left and revert notifications`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = true + val exception = RuntimeException("Network error") + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } throws exception + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isLeft()).isTrue() + result.onLeft { throwable -> + assertThat(throwable).isEqualTo(exception) + } + coVerify { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } + } + + @Test + fun `GIVEN syncTokens throws exception WHEN invoke THEN return Left and revert notifications`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = true + val exception = RuntimeException("Sync error") + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } just runs + coEvery { accountsCRUDRepository.syncTokens(userWalletId) } throws exception + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isLeft()).isTrue() + result.onLeft { throwable -> + assertThat(throwable).isEqualTo(exception) + } + coVerify { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } + } + + @Test + fun `GIVEN disabling notifications fails WHEN invoke THEN return Left and revert to enabled`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = false + val exception = RuntimeException("Network error") + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } throws exception + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isLeft()).isTrue() + result.onLeft { throwable -> + assertThat(throwable).isEqualTo(exception) + } + coVerify { walletsRepository.setNotificationsEnabled(userWalletId, true) } + } +} \ No newline at end of file diff --git a/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt index 50c01a8d05..7d8627e929 100644 --- a/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt +++ b/features/account/api/src/main/java/com/tangem/features/account/PortfolioSelectorComponent.kt @@ -60,5 +60,7 @@ interface PortfolioSelectorController { suspend fun isAccountModeSync(): Boolean fun selectAccount(accountId: AccountId?) - fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow?> + fun selectedAccountWithData( + portfolioFetcher: PortfolioFetcher, + ): Flow?> } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt index 5d48231c07..792db8d675 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/ui/AccountCreateEditContent.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.vectorResource @@ -39,6 +40,7 @@ import com.tangem.core.ui.components.fields.AutoSizeTextField import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.features.account.createedit.entity.AccountCreateEditUM import com.tangem.features.account.createedit.entity.AccountCreateEditUM.Account @@ -53,11 +55,12 @@ internal fun AccountCreateEditContent( ) { val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current + val nestedScrollConnection = rememberHideKeyboardNestedScrollConnection() + Column( modifier = modifier .background(color = TangemTheme.colors.background.secondary) .fillMaxSize() - .imePadding() .systemBarsPadding(), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -65,6 +68,7 @@ internal fun AccountCreateEditContent( Column( modifier = Modifier + .nestedScroll(nestedScrollConnection) .verticalScroll(rememberScrollState()) .padding(horizontal = 16.dp) .weight(1f), diff --git a/features/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/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt b/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt index fe9a055c3f..c125505737 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/selector/DefaultPortfolioSelectorController.kt @@ -3,6 +3,7 @@ package com.tangem.features.account.selector import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.account.PortfolioFetcher import com.tangem.features.account.PortfolioSelectorController @@ -35,21 +36,23 @@ internal class DefaultPortfolioSelectorController @Inject constructor( _selectedAccount.tryEmit(accountId) } - override fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow?> = - combine( - flow = _selectedAccount, - flow2 = portfolioFetcher.data, - transform = { accountId, data -> - accountId ?: return@combine null - var result: Pair? = null + override fun selectedAccountWithData( + portfolioFetcher: PortfolioFetcher, + ): Flow?> = combine( + flow = _selectedAccount, + flow2 = portfolioFetcher.data, + transform = { accountId, data -> + accountId ?: return@combine null + var result: Pair? = null - data.balances.forEach { wallet, balance -> - val accountStatuses = balance.accountsBalance.accountStatuses - .find { accountId == it.account.accountId } - if (accountStatuses != null) result = balance.userWallet to accountStatuses - } + data.balances.forEach { wallet, balance -> + val accountStatuses = balance.accountsBalance.accountStatuses + .filterCryptoPortfolio() + .find { accountId == it.account.accountId } + if (accountStatuses != null) result = balance.userWallet to accountStatuses + } - return@combine result - }, - ) + return@combine result + }, + ) } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt index fc13e4df56..d7e351ff96 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/selector/PortfolioSelectorModel.kt @@ -184,6 +184,7 @@ internal class PortfolioSelectorModel @Inject constructor( val account = accountStatus.account val accountBalance = when (accountStatus) { is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance + is AccountStatus.Payment -> accountStatus.totalFiatBalance } val accountItemUM = AccountPortfolioItemUMConverter( onClick = { selectorController.selectAccount(account.accountId) }, diff --git a/features/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..ac00b3703e 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt @@ -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), @@ -189,6 +186,7 @@ internal class CreateWalletStartModel @Inject constructor( scanCardProcessor.scan( analyticsSource = analyticsSource, + shouldCheckIsAlreadyActivated = true, onProgressStateChange = { showProgress -> if (!showProgress) { delay(HIDE_PROGRESS_DELAY) diff --git a/features/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/build.gradle.kts b/features/details/impl/build.gradle.kts index bd2572246b..19d925cad1 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -69,6 +69,7 @@ dependencies { implementation(deps.compose.material3) implementation(deps.compose.shimmer) implementation(deps.compose.coil) + implementation(deps.compose.reorderableV2) /* DI */ implementation(deps.hilt.android) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/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/component/preview/PreviewUserWalletListComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt index 86ecb2a42f..fb299d1548 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewUserWalletListComponent.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.features.details.component.UserWalletListComponent import com.tangem.features.details.entity.UserWalletListUM +import com.tangem.features.details.entity.WalletReorderUM import com.tangem.features.details.impl.R import com.tangem.features.details.ui.UserWalletListBlock import kotlinx.collections.immutable.persistentListOf @@ -51,6 +52,11 @@ internal class PreviewUserWalletListComponent : UserWalletListComponent { addNewWalletText = resourceReference(R.string.user_wallet_list_add_button), isWalletSavingInProgress = true, onAddNewWalletClick = {}, + walletReorderUM = WalletReorderUM( + isDragEnabled = true, + onMove = { _, _ -> }, + onDragStopped = {}, + ), ), ) diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt index a8ef5eb141..375c63345a 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/entity/UserWalletListUM.kt @@ -11,4 +11,11 @@ internal data class UserWalletListUM( val isWalletSavingInProgress: Boolean, val addNewWalletText: TextReference, val onAddNewWalletClick: () -> Unit, + val walletReorderUM: WalletReorderUM, +) + +internal data class WalletReorderUM( + val isDragEnabled: Boolean, + val onMove: (fromIndex: Int, toIndex: Int) -> Unit, + val onDragStopped: () -> Unit, ) \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/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..44af704d49 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/UserWalletListModel.kt @@ -13,16 +13,20 @@ 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.analytics.WalletSettingsAnalyticEvents +import com.tangem.domain.wallets.usecase.ApplyUserWalletListSortingUseCase import com.tangem.domain.wallets.usecase.UnlockWalletUseCase import com.tangem.features.details.entity.UserWalletListUM +import com.tangem.features.details.entity.WalletReorderUM import com.tangem.features.details.impl.R import com.tangem.features.details.utils.UserWalletSaver import com.tangem.features.hotwallet.HotWalletFeatureToggles +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber @@ -32,7 +36,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, @@ -40,6 +43,8 @@ internal class UserWalletListModel @Inject constructor( private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val unlockWalletUseCase: UnlockWalletUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + private val walletFeatureToggles: WalletFeatureToggles, + private val applyUserWalletListSortingUseCase: ApplyUserWalletListSortingUseCase, ) : Model() { private val isWalletSavingInProgress: MutableStateFlow = MutableStateFlow(value = false) @@ -47,7 +52,7 @@ internal class UserWalletListModel @Inject constructor( messageSender = messageSender, onlyMultiCurrency = false, isAuthMode = false, - isClickableIfLocked = hotWalletFeatureToggles.isHotWalletEnabled, + isClickableIfLocked = true, onWalletClick = ::onWalletClicked, ) @@ -57,6 +62,11 @@ internal class UserWalletListModel @Inject constructor( isWalletSavingInProgress = false, addNewWalletText = TextReference.EMPTY, onAddNewWalletClick = ::onAddNewWalletClick, + walletReorderUM = WalletReorderUM( + isDragEnabled = false, + onMove = ::onWalletReorder, + onDragStopped = ::onWalletDragStopped, + ), ), ) @@ -67,67 +77,73 @@ 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), + walletReorderUM = WalletReorderUM( + isDragEnabled = walletFeatureToggles.isWalletReorderFeatureEnabled && userWallets.size > 1, + onMove = ::onWalletReorder, + onDragStopped = ::onWalletDragStopped, + ), + ) + } 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, - ) - } + 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, + ) + } + } + } + + private fun onWalletReorder(fromIndex: Int, toIndex: Int) { + state.update { prevState -> + val wallets = prevState.userWallets.toMutableList() + wallets.add(toIndex, wallets.removeAt(fromIndex)) + prevState.copy(userWallets = wallets.toPersistentList()) + } + } + + private fun onWalletDragStopped() { + val userWalletIds = state.value.userWallets.map { UserWalletId(it.id) } + + modelScope.launch { + applyUserWalletListSortingUseCase(userWalletIds).onRight { + analyticsEventHandler.send(WalletSettingsAnalyticEvents.WalletsReorder()) + }.onLeft { error -> + Timber.e("Failed to apply wallet list sorting: $error") } - } else { - router.push(AppRoute.WalletSettings(userWalletId)) } } } \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt index e253ffec97..2cff0e2d16 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/ui/UserWalletListBlock.kt @@ -2,17 +2,25 @@ package com.tangem.features.details.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.key +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview @@ -20,6 +28,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.common.ui.userwallet.UserWalletItem +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference @@ -28,26 +37,84 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.details.component.UserWalletListComponent import com.tangem.features.details.component.preview.PreviewUserWalletListComponent import com.tangem.features.details.entity.UserWalletListUM +import com.tangem.features.details.entity.WalletReorderUM import com.tangem.features.details.impl.R +import sh.calvin.reorderable.ReorderableItem +import sh.calvin.reorderable.ReorderableLazyListState +import sh.calvin.reorderable.rememberReorderableLazyListState @Composable internal fun UserWalletListBlock(state: UserWalletListUM, modifier: Modifier = Modifier) { + val listState = rememberLazyListState() + val reorderableListState = rememberReorderableLazyListState( + lazyListState = listState, + onMove = { from, to -> state.walletReorderUM.onMove(from.index, to.index) }, + ) + + val listHeight = WALLET_ITEM_HEIGHT * state.userWallets.size + ADD_WALLET_BUTTON_HEIGHT + BlockCard( modifier = modifier, ) { - state.userWallets.forEach { state -> - key(state.id) { - UserWalletItem( + LazyColumn( + state = listState, + modifier = Modifier + .heightIn(max = listHeight), + ) { + items( + items = state.userWallets, + key = { it.id }, + ) { walletState -> + WalletItem( + model = walletState, + reorderableListState = reorderableListState, + walletReorderUM = state.walletReorderUM, modifier = Modifier.fillMaxWidth(), - state = state, + ) + } + item(key = "add_wallet_button") { + AddWalletButton( + text = state.addNewWalletText, + isInProgress = state.isWalletSavingInProgress, + onClick = state.onAddNewWalletClick, ) } } - AddWalletButton( - text = state.addNewWalletText, - isInProgress = state.isWalletSavingInProgress, - onClick = state.onAddNewWalletClick, - ) + } +} + +@Suppress("MagicNumber") +@Composable +private fun LazyItemScope.WalletItem( + model: UserWalletItemUM, + reorderableListState: ReorderableLazyListState, + walletReorderUM: WalletReorderUM, + modifier: Modifier = Modifier, +) { + ReorderableItem( + state = reorderableListState, + key = model.id, + modifier = modifier, + ) { isDragging -> + val elevation by animateDpAsState(if (isDragging) 9.dp else 0.dp) + val scale by animateFloatAsState(if (isDragging) 1.04f else 1f) + val shapeRadius by animateDpAsState(if (isDragging) 16.dp else 0.dp) + + Surface( + shape = RoundedCornerShape(shapeRadius), + shadowElevation = elevation, + modifier = Modifier.scale(scale), + ) { + UserWalletItem( + state = model, + modifier = Modifier + .longPressDraggableHandle( + enabled = walletReorderUM.isDragEnabled, + onDragStopped = walletReorderUM.onDragStopped, + ) + .background(color = TangemTheme.colors.background.primary), + ) + } } } @@ -109,6 +176,9 @@ private fun AddWalletButton( } } +private val WALLET_ITEM_HEIGHT = 72.dp +private val ADD_WALLET_BUTTON_HEIGHT = 60.dp + // region Preview @Composable @Preview(showBackground = true, widthDp = 360) diff --git a/features/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..3e81799932 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/utils/UserWalletSaver.kt @@ -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 } @@ -126,6 +120,7 @@ internal class UserWalletSaver @Inject constructor( scope.launch { scanCardProcessor.scan( analyticsSource = AnalyticsParam.ScreensSources.Settings, + shouldCheckIsAlreadyActivated = true, onWalletNotCreated = { continuation.resume(Either.Right(null)) }, diff --git a/features/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..c7feac3a8c 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt @@ -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 { @@ -15,13 +16,17 @@ interface AddToPortfolioPreselectedDataComponent : ComposableBottomSheetComponen data class Params( val tokenToAdd: TokenToAdd, val callback: Callback, + val analyticsParams: AnalyticsParams, ) + data class AnalyticsParams(val source: String) + interface Callback { fun onDismiss() fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId) } + @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..02b1c82ab7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -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,12 +102,14 @@ internal class FeedEntryChildFactory @Inject constructor( DefaultFeedComponent( appComponentContext = appComponentContext, params = DefaultFeedComponent.FeedParams(feedClickIntents = feedEntryClickIntents), + addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent, ) } is Child.Earn -> { DefaultEarnComponent( appComponentContext = appComponentContext, params = child.params, + addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent, ) } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt index ad41d329a1..a0b7dcb1e8 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt @@ -5,15 +5,22 @@ import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.R import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.features.feed.components.feed.FeedBottomSheetRoute +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.model.earn.EarnModel import com.tangem.features.feed.ui.earn.EarnContent import kotlinx.serialization.Serializable @@ -21,10 +28,18 @@ import kotlinx.serialization.Serializable internal class DefaultEarnComponent( appComponentContext: AppComponentContext, private val params: Params, + private val addToPortfolioComponentFactory: AddToPortfolioPreselectedDataComponent.Factory, ) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext { private val earnModel = getOrCreateModel(params = params) + private val bottomSheetSlot = childSlot( + source = earnModel.bottomSheetNavigation, + serializer = null, + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + @Composable override fun Title(bottomSheetState: State) { val background = LocalMainBottomSheetColor.current.value @@ -42,11 +57,38 @@ internal class DefaultEarnComponent( @Composable override fun Content(bottomSheetState: State, modifier: Modifier) { + val bottomSheet by bottomSheetSlot.subscribeAsState() val state by earnModel.state.collectAsStateWithLifecycle() + EarnContent( state = state, modifier = modifier, ) + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + config: FeedBottomSheetRoute, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = when (config) { + is FeedBottomSheetRoute.AddToPortfolio -> { + addToPortfolioComponentFactory.create( + context = childByContext(componentContext), + params = AddToPortfolioPreselectedDataComponent.Params( + tokenToAdd = config.tokenToAdd, + callback = earnModel.addToPortfolioCallback, + analyticsParams = AddToPortfolioPreselectedDataComponent.AnalyticsParams(config.source), + ), + ) + } + is FeedBottomSheetRoute.NetworkFilter -> EarnNetworkFilterComponent( + context = childByContext(componentContext), + params = config.params, + ) + is FeedBottomSheetRoute.TypeFilter -> EarnTypeFilterComponent( + context = childByContext(componentContext), + params = config.params, + ) } @Serializable diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/EarnNetworkFilterComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/EarnNetworkFilterComponent.kt new file mode 100644 index 0000000000..bf3b30b651 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/EarnNetworkFilterComponent.kt @@ -0,0 +1,43 @@ +package com.tangem.features.feed.components.earn + +import androidx.compose.runtime.Composable +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.earn.model.EarnFilterNetwork +import com.tangem.features.feed.model.earn.filters.EarnNetworkFilterModel +import com.tangem.features.feed.ui.earn.components.EarnFilterByNetworkBottomSheet +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +internal class EarnNetworkFilterComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: Params, +) : AppComponentContext by context, ComposableBottomSheetComponent { + + private val model = getOrCreateModel(params = params) + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val state = model.state.collectAsStateWithLifecycle() + EarnFilterByNetworkBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = state.value, + ), + ) + } + + data class Params( + val allFilters: List, + val onFilterSelected: (EarnFilterNetwork) -> Unit, + val onDismiss: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/EarnTypeFilterComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/EarnTypeFilterComponent.kt new file mode 100644 index 0000000000..de290cb1e3 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/EarnTypeFilterComponent.kt @@ -0,0 +1,44 @@ +package com.tangem.features.feed.components.earn + +import androidx.compose.runtime.Composable +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.domain.earn.model.EarnFilterType +import com.tangem.features.feed.model.earn.filters.EarnTypeFilterModel +import com.tangem.features.feed.ui.earn.components.EarnFilterByTypeBottomSheet +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +internal class EarnTypeFilterComponent @AssistedInject constructor( + @Assisted context: AppComponentContext, + @Assisted private val params: Params, +) : AppComponentContext by context, ComposableBottomSheetComponent { + + private val model = getOrCreateModel(params = params) + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val state = model.state.collectAsStateWithLifecycle() + + EarnFilterByTypeBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = state.value, + ), + ) + } + + data class Params( + val selectedFilter: EarnFilterType, + val onFilterSelected: (EarnFilterType) -> Unit, + val onDismiss: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt index f7073e2ca2..7fbaa5c4fe 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt @@ -6,10 +6,19 @@ 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.analytics.models.AnalyticsParam 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.core.ui.decompose.EmptyComposableBottomSheetComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent.Params import com.tangem.features.feed.model.feed.FeedComponentModel import com.tangem.features.feed.model.feed.FeedModelClickIntents import com.tangem.features.feed.ui.feed.FeedList @@ -18,10 +27,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 = null, + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + @Composable override fun Title(bottomSheetState: State) { val state by feedComponentModel.state.collectAsStateWithLifecycle() @@ -40,11 +57,33 @@ 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: FeedBottomSheetRoute, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = when (config) { + is FeedBottomSheetRoute.AddToPortfolio -> { + addToPortfolioComponentFactory.create( + context = childByContext(componentContext), + params = Params( + tokenToAdd = config.tokenToAdd, + callback = feedComponentModel.addToPortfolioCallback, + analyticsParams = AddToPortfolioPreselectedDataComponent.AnalyticsParams( + AnalyticsParam.ScreensSources.Markets.value, + ), + ), + ) + } + is FeedBottomSheetRoute.NetworkFilter -> EmptyComposableBottomSheetComponent + is FeedBottomSheetRoute.TypeFilter -> EmptyComposableBottomSheetComponent } data class FeedParams(val feedClickIntents: FeedModelClickIntents) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt new file mode 100644 index 0000000000..fea9189d03 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt @@ -0,0 +1,17 @@ +package com.tangem.features.feed.components.feed + +import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent +import com.tangem.features.feed.components.earn.EarnTypeFilterComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent + +internal sealed interface FeedBottomSheetRoute { + + data class AddToPortfolio( + val tokenToAdd: AddToPortfolioPreselectedDataComponent.TokenToAdd, + val source: String, + ) : FeedBottomSheetRoute + + data class NetworkFilter(val params: EarnNetworkFilterComponent.Params) : FeedBottomSheetRoute + + data class TypeFilter(val params: EarnTypeFilterComponent.Params) : FeedBottomSheetRoute +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/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/AddToPortfolioModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt index a06d46878d..0d733ab09f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -157,6 +157,8 @@ internal class AddToPortfolioModel @Inject constructor( allRequireForAdd.first() // line of navigation to AddToken screen is finished; cancel the job, select a new root screen firstPartOfNavigation.cancel() + + analyticsEventHandler.send(event = eventBuilder.popupToConfirm()) navigation.replaceAll(AddToPortfolioRoutes.AddToken) var middleNavigationJob: Job? = null @@ -325,6 +327,7 @@ internal class AddToPortfolioModel @Inject constructor( ): CryptoCurrency? { val accountIndex = when (val accountStatus = account.account) { is AccountStatus.CryptoPortfolio -> accountStatus.account.derivationIndex + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } return getTokenMarketCryptoCurrency( userWalletId = userWallet.walletId, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt index b673414488..79ca66cf2b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt @@ -25,6 +25,7 @@ import com.tangem.features.feed.components.market.details.portfolio.add.* import com.tangem.features.feed.components.market.details.portfolio.add.impl.AddTokenComponent import com.tangem.features.feed.components.market.details.portfolio.impl.analytics.PortfolioAnalyticsEvent import com.tangem.features.feed.impl.R +import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* @@ -112,8 +113,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( @@ -123,9 +126,11 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor( return@channelFlow } + sendAddTokenOpenedAnalytics(selectedNetworkValue.cryptoCurrency) navigation.replaceAll(AddToPortfolioRoutes.AddToken) val addedToken = callbackDelegate.onTokenAdded.receiveAsFlow().first() messageSender.send(ToastMessage(message = resourceReference(R.string.markets_token_added))) + sendSuccessAddedAnalytics(addedToken.currency) finishSuccessFlow(addedToken.currency, selectedPortfolioValue.userWallet.walletId) } .catch { throwable -> @@ -165,13 +170,14 @@ 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 val availableToAddAccounts = accounts.mapNotNull { accountStatus -> val accountIndex = when (accountStatus) { is AccountStatus.CryptoPortfolio -> accountStatus.account.derivationIndex + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } val cryptoCurrency = getTokenMarketCryptoCurrency( @@ -206,9 +212,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( @@ -218,6 +224,7 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor( ): CryptoCurrency? { val accountIndex = when (val accountStatus = account.account) { is AccountStatus.CryptoPortfolio -> accountStatus.account.derivationIndex + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } return getTokenMarketCryptoCurrency( userWalletId = userWallet.walletId, @@ -263,6 +270,25 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor( }, ) .filterNotNull() + + private fun sendSuccessAddedAnalytics(cryptoCurrency: CryptoCurrency) { + analyticsEventHandler.send( + EarnAnalyticsEvent.TokenAdded( + tokenSymbol = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + ), + ) + } + + private fun sendAddTokenOpenedAnalytics(cryptoCurrency: CryptoCurrency) { + analyticsEventHandler.send( + EarnAnalyticsEvent.AddTokenScreenOpened( + tokenSymbol = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + source = params.analyticsParams.source, + ), + ) + } } @ModelScoped diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt index d616bf53be..17d27f1aad 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt @@ -52,7 +52,7 @@ internal class AddTokenModel @Inject constructor( flow = params.selectedNetwork.distinctUntilChanged(), flow2 = params.selectedPortfolio.distinctUntilChanged(), transform = { selectedNetwork, selectedPortfolio -> - addTokenJob.cancel() + addTokenJob.join() val isTangemIconVisible = needColdWalletInteraction(selectedNetwork, selectedPortfolio) uiBuilder.updateContent( selectedPortfolio = selectedPortfolio, @@ -88,6 +88,7 @@ internal class AddTokenModel @Inject constructor( val blockchainNames = listOf(selectedNetwork.selectedNetwork) .mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name } analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames)) + analyticsEventHandler.send(analyticsEventBuilder.addButtonClick()) manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency) .onLeft { throwable -> @@ -110,6 +111,11 @@ internal class AddTokenModel @Inject constructor( } is Account.Payment -> TODO("[REDACTED_JIRA]") } + + analyticsEventHandler.send( + event = analyticsEventBuilder.tokenAdded(status.status.currency.network.name), + ) + params.callbacks.onTokenAdded(status.status) } uiState.value = um.toggleProgress(false) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenUiBuilder.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenUiBuilder.kt index 5d5c7e379e..2055a204ab 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenUiBuilder.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenUiBuilder.kt @@ -13,7 +13,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.iconResId import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.AccountStatus.* import com.tangem.features.feed.components.market.details.portfolio.add.SelectedNetwork import com.tangem.features.feed.components.market.details.portfolio.add.SelectedPortfolio import com.tangem.features.feed.components.market.details.portfolio.add.impl.AddTokenComponent @@ -36,7 +36,7 @@ internal class AddTokenUiBuilder @Inject constructor( } private fun createPortfolio(selectedPortfolio: SelectedPortfolio): PortfolioSelectUM { - val accountIcon: AccountIconUM.CryptoPortfolio? + val accountIcon: AccountIconUM? val portfolioName: TextReference when (selectedPortfolio.isAccountMode) { false -> { @@ -47,7 +47,8 @@ internal class AddTokenUiBuilder @Inject constructor( val accountStatus = selectedPortfolio.account.account portfolioName = accountStatus.account.accountName.toUM().value accountIcon = when (accountStatus) { - is AccountStatus.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon) + is CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon) + is Payment -> AccountIconUM.Payment } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt index 8ba89fb021..feb3025f69 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt @@ -66,7 +66,7 @@ class CheckCurrencyUnsupportedDelegate @Inject constructor( formatArgs = wrappedList(unsupportedState.networkName), ) is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, + id = R.string.alert_manage_tokens_unsupported_blockchain_by_card_message, formatArgs = wrappedList(unsupportedState.networkName), ) }, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt index b75d67abf4..87ed9607b7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt @@ -15,28 +15,64 @@ internal class PortfolioAnalyticsEvent( fun addToPortfolioClicked() = PortfolioAnalyticsEvent( event = "Button - Add To Portfolio", - params = mapOf( - "Token" to tokenSymbol, - ), + params = buildMap { + put("Token", tokenSymbol) + if (source != null) put("Source", source) + }, ) fun popupToChooseAccount() = PortfolioAnalyticsEvent( event = "Choose Account Opened", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun popupToConfirm() = PortfolioAnalyticsEvent( + event = "Add Token Screen Opened", + params = buildMap { + if (source != null) put("Source", source) + }, ) fun addToNotMainAccount() = PortfolioAnalyticsEvent( event = "Button - Add To Account", + params = buildMap { + if (source != null) put("Source", source) + }, ) - fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(event = "Wallet Selected") + fun addButtonClick() = PortfolioAnalyticsEvent( + event = "Button - Add Token", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent( + event = "Wallet Selected", + params = buildMap { + if (source != null) put("Source", source) + }, + ) fun addToPortfolioContinue(blockchainNames: List) = PortfolioAnalyticsEvent( event = "Token Network Selected", - params = mapOf( - "Count" to blockchainNames.size.toString(), - "Token" to tokenSymbol, - "blockchain" to blockchainNames.joinToString(separator = ", "), - ), + params = buildMap { + put("Count", blockchainNames.size.toString()) + put("Token", tokenSymbol) + put("blockchain", blockchainNames.joinToString(separator = ", ")) + if (source != null) put("Source", source) + }, + ) + + fun tokenAdded(blockchainName: String) = PortfolioAnalyticsEvent( + event = "Token Added", + params = buildMap { + put("Token", tokenSymbol) + put("Blockchain", blockchainName) + if (source != null) put("Source", source) + }, ) fun quickActionClick(actionUM: TokenActionsBSContentUM.Action, blockchainName: String) = @@ -51,7 +87,7 @@ internal class PortfolioAnalyticsEvent( }, params = buildMap { put("Token", tokenSymbol) - source?.let { put("Source", it) } + if (source != null) put("Source", source) put("blockchain", blockchainName) }, ) @@ -64,10 +100,16 @@ internal class PortfolioAnalyticsEvent( TokenActionsBSContentUM.Action.Stake -> "Popup Get token - Button Stake" else -> "error" }, + params = buildMap { + if (source != null) put("Source", source) + }, ) fun getTokenLater() = PortfolioAnalyticsEvent( event = "Popup Get token - Button Later", + params = buildMap { + if (source != null) put("Source", source) + }, ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/NewMarketsPortfolioDelegate.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/NewMarketsPortfolioDelegate.kt index 21a34d471c..2df91aab47 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/NewMarketsPortfolioDelegate.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/NewMarketsPortfolioDelegate.kt @@ -202,6 +202,7 @@ internal class NewMarketsPortfolioDelegate @AssistedInject constructor( val currencyId = status.currency.id.rawCurrencyId ?: return@filter false getTokenIdIfL2Network(currencyId.value) == currencyRawId.value } + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } return accountStatuses.map { accountStatus -> AccountWithAdded( diff --git a/features/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/di/ModelModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt index f81263e79f..2fee6f7d0b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt @@ -4,6 +4,8 @@ import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.feed.model.FeedEntryModel import com.tangem.features.feed.model.earn.EarnModel +import com.tangem.features.feed.model.earn.filters.EarnNetworkFilterModel +import com.tangem.features.feed.model.earn.filters.EarnTypeFilterModel import com.tangem.features.feed.model.feed.FeedComponentModel import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel import com.tangem.features.feed.model.market.list.MarketsListModel @@ -53,4 +55,14 @@ internal interface ModelModule { @IntoMap @ClassKey(FeedEntryModel::class) fun provideFeedEntryModel(model: FeedEntryModel): Model + + @Binds + @IntoMap + @ClassKey(EarnNetworkFilterModel::class) + fun provideEarnNetworkFilterModel(model: EarnNetworkFilterModel): Model + + @Binds + @IntoMap + @ClassKey(EarnTypeFilterModel::class) + fun provideEarnTypeFilterModel(model: EarnTypeFilterModel): Model } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/EarnTokenWithCurrencyToListItemUMConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/EarnTokenWithCurrencyToListItemUMConverter.kt new file mode 100644 index 0000000000..59cadd9281 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/EarnTokenWithCurrencyToListItemUMConverter.kt @@ -0,0 +1,50 @@ +package com.tangem.features.feed.model.converter + +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.domain.models.earn.EarnRewardType +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.domain.models.earn.EarnType +import com.tangem.features.feed.impl.R +import com.tangem.features.feed.ui.earn.state.EarnListItemUM +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class EarnTokenWithCurrencyToListItemUMConverter( + private val onItemClick: (EarnTokenWithCurrency) -> Unit, +) : Converter { + + private val cryptoCurrencyToIconStateConverter = CryptoCurrencyToIconStateConverter() + + override fun convert(value: EarnTokenWithCurrency): EarnListItemUM { + return EarnListItemUM( + network = TextReference.Str(value.networkName), + symbol = TextReference.Str(value.earnToken.tokenSymbol), + tokenName = TextReference.Str(value.earnToken.tokenName), + currencyIconState = cryptoCurrencyToIconStateConverter.convert(value.cryptoCurrency), + earnValue = when (value.earnToken.rewardType) { + EarnRewardType.APR -> TextReference.Res( + id = R.string.staking_apr_earn_badge, + formatArgs = wrappedList(convertPercent(value.earnToken.apy)), + ) + EarnRewardType.APY -> TextReference.Res( + id = R.string.yield_module_earn_badge, + formatArgs = wrappedList(convertPercent(value.earnToken.apy)), + ) + }, + earnType = when (value.earnToken.type) { + EarnType.STAKING -> TextReference.Res(R.string.common_staking) + EarnType.YIELD -> TextReference.Res(R.string.common_yield_mode) + }, + onItemClick = { onItemClick(value) }, + ) + } + + private fun convertPercent(value: String): TextReference { + val percent = BigDecimal(value).format { percent(withPercentSign = false) } + return TextReference.Str(percent) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index f1471cf802..bb91b6a704 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -1,39 +1,329 @@ package com.tangem.features.feed.model.earn import androidx.compose.runtime.Stable +import arrow.core.Either +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.domain.earn.model.EarnFilter +import com.tangem.domain.earn.model.EarnFilterNetwork +import com.tangem.domain.earn.model.EarnFilterType +import com.tangem.domain.earn.usecase.* +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.earn.EarnNetworks +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.feed.components.earn.DefaultEarnComponent -import com.tangem.features.feed.ui.earn.state.EarnListUM +import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent +import com.tangem.features.feed.components.earn.EarnTypeFilterComponent +import com.tangem.features.feed.components.feed.FeedBottomSheetRoute +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent +import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent +import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkConverter +import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConverter +import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeConverter +import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeUMConverter +import com.tangem.features.feed.model.earn.state.EarnStateController +import com.tangem.features.feed.model.earn.state.transformers.EarnFilterSelectedStateTransformer +import com.tangem.features.feed.model.earn.state.transformers.UpdateBestOpportunitiesStateTransformer +import com.tangem.features.feed.model.earn.state.transformers.UpdateEarnUMInitialStateTransformer +import com.tangem.features.feed.model.earn.state.transformers.UpdateMostlyUsedStateTransformer +import com.tangem.features.feed.model.earn.statemanager.EarnListBatchFlowManager +import com.tangem.features.feed.model.earn.statemanager.EarnListStateManager +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM import com.tangem.features.feed.ui.earn.state.EarnUM +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject @Stable @ModelScoped +@Suppress("LongParameterList") internal class EarnModel @Inject constructor( - override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val fetchEarnNetworksUseCase: FetchEarnNetworksUseCase, + private val getEarnNetworksUseCase: GetEarnNetworksUseCase, + private val getEarnTokensBatchFlowUseCase: GetEarnTokensBatchFlowUseCase, + private val getTopEarnTokensUseCase: GetTopEarnTokensUseCase, + private val fetchTopEarnTokensUseCase: FetchTopEarnTokensUseCase, + private val getEarnFilterUseCase: GetEarnFilterUseCase, + private val setEarnFilterUseCase: SetEarnFilterUseCase, + private val appRouter: AppRouter, + private val stateController: EarnStateController, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { private val params = paramsContainer.require() + private val earnNetworks = MutableStateFlow(Either.Right(emptyList())) + private val earnListConfigProvider = Provider { + createEarnTokensListConfig( + selectedTypeFilter = stateController.value.selectedTypeFilter, + selectedNetworkFilter = stateController.value.selectedNetworkFilter, + earnNetworks = earnNetworks.value, + ) + } + + private val batchFlowManager = EarnListBatchFlowManager( + getEarnTokensBatchFlowUseCase = getEarnTokensBatchFlowUseCase, + configProvider = earnListConfigProvider, + onItemClick = ::onEarnTokenClick, + modelScope = modelScope, + dispatchers = dispatchers, + ) + + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + + val addToPortfolioCallback = object : AddToPortfolioPreselectedDataComponent.Callback { + override fun onDismiss() = bottomSheetNavigation.dismiss() + override fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId) { + bottomSheetNavigation.dismiss() + appRouter.push( + AppRoute.CurrencyDetails( + userWalletId = walletId, + currency = addedToken, + ), + ) + } + } val state: StateFlow - field = MutableStateFlow(createInitialState()) + get() = stateController.uiState - private fun createInitialState(): EarnUM = EarnUM( - mostlyUsed = EarnListUM.Loading, - bestOpportunities = EarnListUM.Loading, - selectedNetworkFilter = null, - selectedTypeFilter = null, - networkFilters = persistentListOf(), - typeFilters = persistentListOf(), - onBackClick = params.onBackClick, - onNetworkFilterClick = { }, - onTypeFilterClick = { }, - ) + init { + updateInitialState() + fetchEarnNetworks() + fetchTopEarnTokens() + subscribeOnStoredFilters() + subscribeOnNetworks() + subscribeOnBatchFlow() + subscribeToMostlyUsed() + } + + private fun subscribeOnBatchFlow() { + combine( + batchFlowManager.uiItems, + batchFlowManager.initialLoadingError, + batchFlowManager.paginationStatus, + ) { items, error, paginationStatus -> + val hasActiveFilters = state.value.selectedTypeFilter != EarnFilterTypeUM.All || + state.value.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks + error?.let(::handleBestOpportunitiesErrorAnalytics) + EarnListStateManager.calculateState( + items = items, + error = error, + paginationStatus = paginationStatus, + hasActiveFilters = hasActiveFilters, + onRetryClick = { batchFlowManager.reload() }, + onLoadMore = { batchFlowManager.loadMore() }, + onClearFiltersClick = ::onClearFiltersClick, + ) + }.onEach { bestOpportunitiesState -> + stateController.update(UpdateBestOpportunitiesStateTransformer(bestOpportunitiesState)) + }.launchIn(modelScope) + } + + private fun subscribeToMostlyUsed() { + modelScope.launch(dispatchers.default) { + getTopEarnTokensUseCase().collect { earnResult -> + stateController.update( + UpdateMostlyUsedStateTransformer( + earnResult = earnResult, + onItemClick = ::onEarnTokenClick, + onRetryClick = ::fetchTopEarnTokens, + ), + ) + } + } + } + + private fun subscribeOnNetworks() { + modelScope.launch(dispatchers.default) { + getEarnNetworksUseCase().collect(earnNetworks) + } + } + + private fun subscribeOnStoredFilters() { + modelScope.launch(dispatchers.default) { + getEarnFilterUseCase() + .collect { filter -> + val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType) + val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork) + stateController.update( + EarnFilterSelectedStateTransformer( + filterType = typeFilterUM, + filterNetwork = networkFilterUM, + ), + ) + batchFlowManager.reload() + } + } + } + + private fun fetchTopEarnTokens() { + modelScope.launch(dispatchers.default) { + fetchTopEarnTokensUseCase() + } + } + + private fun fetchEarnNetworks() { + modelScope.launch(dispatchers.default) { + fetchEarnNetworksUseCase() + } + } + + /* start of clicks area */ + private fun onTypeFilterClick() { + val currentState = state.value + bottomSheetNavigation.activate( + FeedBottomSheetRoute.TypeFilter( + params = EarnTypeFilterComponent.Params( + selectedFilter = EarnFilterTypeUMConverter().convert(currentState.selectedTypeFilter), + onFilterSelected = ::onTypeFilterOptionSelected, + onDismiss = { bottomSheetNavigation.dismiss() }, + ), + ), + ) + } + + private fun onNetworkFilterClick() { + bottomSheetNavigation.activate( + FeedBottomSheetRoute.NetworkFilter( + params = EarnNetworkFilterComponent.Params( + allFilters = createNetworkFilters(), + onFilterSelected = ::onNetworkFilterOptionSelected, + onDismiss = { bottomSheetNavigation.dismiss() }, + ), + ), + ) + } + + private fun createNetworkFilters(): List { + val selectedFilter = state.value.selectedNetworkFilter + return buildList { + add( + EarnFilterNetwork.AllNetworks( + isSelected = selectedFilter is EarnFilterNetworkUM.AllNetworks, + ), + ) + add( + EarnFilterNetwork.MyNetworks( + isSelected = selectedFilter is EarnFilterNetworkUM.MyNetworks, + ), + ) + earnNetworks.value.onRight { networks -> + networks.mapTo(this) { network -> + EarnFilterNetwork.Specific( + id = network.networkId, + isSelected = selectedFilter is EarnFilterNetworkUM.Network && + selectedFilter.id == network.networkId, + symbol = network.symbol, + fullName = network.fullName, + ) + } + } + } + } + + private fun onClearFiltersClick() { + modelScope.launch(dispatchers.default) { + setEarnFilterUseCase( + EarnFilter( + earnFilterNetwork = EarnFilterNetwork.AllNetworks(isSelected = true), + earnFilterType = EarnFilterType.ALL, + ), + ) + } + } + + private fun onEarnTokenClick(earnTokenWithCurrency: EarnTokenWithCurrency, source: String) { + analyticsEventHandler.send( + EarnAnalyticsEvent.OpportunitySelected( + tokenSymbol = earnTokenWithCurrency.earnToken.tokenSymbol, + blockchain = earnTokenWithCurrency.cryptoCurrency.network.name, + source = source, + ), + ) + bottomSheetNavigation.activate( + FeedBottomSheetRoute.AddToPortfolio( + tokenToAdd = AddToPortfolioPreselectedDataComponent.TokenToAdd( + 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, + ), + source = source, + ), + ) + } + + private fun onTypeFilterOptionSelected(type: EarnFilterType) { + modelScope.launch(dispatchers.default) { + setEarnFilterUseCase( + EarnFilter( + earnFilterNetwork = EarnFilterNetworkUMConverter().convert(state.value.selectedNetworkFilter), + earnFilterType = type, + ), + ) + bottomSheetNavigation.dismiss() + } + } + + private fun onNetworkFilterOptionSelected(filter: EarnFilterNetwork) { + modelScope.launch(dispatchers.default) { + setEarnFilterUseCase( + EarnFilter( + earnFilterNetwork = filter, + earnFilterType = EarnFilterTypeUMConverter().convert(state.value.selectedTypeFilter), + ), + ) + } + bottomSheetNavigation.dismiss() + } + + private fun onMostlyUsedScrolled() { + analyticsEventHandler.send(EarnAnalyticsEvent.MostlyUsedCarouselScrolled()) + } + /* end of clicks area */ + + private fun updateInitialState() { + analyticsEventHandler.send(EarnAnalyticsEvent.EarnOpened()) + stateController.update( + UpdateEarnUMInitialStateTransformer( + onBackClick = params.onBackClick, + onNetworkFilterClick = ::onNetworkFilterClick, + onTypeFilterClick = ::onTypeFilterClick, + onScroll = ::onMostlyUsedScrolled, + ), + ) + } + + private fun handleBestOpportunitiesErrorAnalytics(error: Throwable) { + val (code, message) = when (error) { + is ApiResponseError.HttpException -> error.code.numericCode to error.message.orEmpty() + else -> null to "" + } + analyticsEventHandler.send( + EarnAnalyticsEvent.BestOpportunitiesLoadError( + code = code, + message = message, + ), + ) + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt new file mode 100644 index 0000000000..08845ff277 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt @@ -0,0 +1,37 @@ +package com.tangem.features.feed.model.earn + +import com.tangem.domain.earn.model.EarnTokensListConfig +import com.tangem.domain.models.earn.EarnNetwork +import com.tangem.domain.models.earn.EarnNetworks +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM + +internal fun createEarnTokensListConfig( + selectedTypeFilter: EarnFilterTypeUM, + selectedNetworkFilter: EarnFilterNetworkUM, + earnNetworks: EarnNetworks, + isForEarn: Boolean = false, +): EarnTokensListConfig { + val type = when (selectedTypeFilter) { + EarnFilterTypeUM.All -> null + EarnFilterTypeUM.Staking -> "staking" + EarnFilterTypeUM.YieldMode -> "yield" + } + val networks = when (selectedNetworkFilter) { + is EarnFilterNetworkUM.AllNetworks -> null + is EarnFilterNetworkUM.MyNetworks -> { + earnNetworks.fold( + ifLeft = { null }, + ifRight = { networks -> + networks.filter(EarnNetwork::isAdded).map(EarnNetwork::networkId) + }, + ) + } + is EarnFilterNetworkUM.Network -> listOf(selectedNetworkFilter.id) + } + return EarnTokensListConfig( + type = type, + networks = networks, + isForEarn = isForEarn, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt new file mode 100644 index 0000000000..c608bc31c7 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt @@ -0,0 +1,102 @@ +package com.tangem.features.feed.model.earn.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE +import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE +import com.tangem.core.analytics.models.IS_NOT_HTTP_ERROR +import com.tangem.core.analytics.models.OneTimePerSessionEvent + +internal sealed class EarnAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category = "Earn", event = event, params = params) { + + class EarnOpened : EarnAnalyticsEvent(event = "Page Opened") + + class MostlyUsedCarouselScrolled : EarnAnalyticsEvent(event = "Mostly Used Carousel Scrolled"), + OneTimePerSessionEvent { + override val oneTimeEventId: String = event + } + + data class BestOpportunitiesFilterNetworkApplied( + private val networkId: String?, + private val filterType: FilterNetworkAnalytic, + ) : EarnAnalyticsEvent( + event = "Best Opportunities Filter Network Applied", + params = mapOf( + "Network Filter Type" to filterType.value, + "NetworkId" to networkId.orEmpty(), + ), + ) + + data class BestOpportunitiesFilterTypeApplied( + private val filterTypeAnalytic: FilterTypeAnalytic, + ) : EarnAnalyticsEvent( + event = "Best Opportunities Filter Type Applied", + params = mapOf("Type" to filterTypeAnalytic.value), + ) + + data class OpportunitySelected( + private val tokenSymbol: String, + private val blockchain: String, + private val source: String, + ) : EarnAnalyticsEvent( + event = "Opportunity selected", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to tokenSymbol, + AnalyticsParam.BLOCKCHAIN to blockchain, + AnalyticsParam.SOURCE to source, + ), + ) + + data class AddTokenScreenOpened( + private val tokenSymbol: String, + private val blockchain: String, + private val source: String, + ) : EarnAnalyticsEvent( + event = "Add Token Screen Opened", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to tokenSymbol, + AnalyticsParam.BLOCKCHAIN to blockchain, + AnalyticsParam.SOURCE to source, + ), + ) + + data class TokenAdded( + private val tokenSymbol: String, + private val blockchain: String, + ) : EarnAnalyticsEvent( + event = "Token Added", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to tokenSymbol, + AnalyticsParam.BLOCKCHAIN to blockchain, + ), + ) + + data class BestOpportunitiesLoadError( + private val code: Int?, + private val message: String, + ) : EarnAnalyticsEvent( + event = "Best Opportunities Load Error", + params = mapOf( + ERROR_CODE to (code ?: IS_NOT_HTTP_ERROR).toString(), + ERROR_MESSAGE to message, + ), + ) +} + +internal const val BEST_OPPORTUNITIES_SOURCE = "Best Opportunity" +internal const val MOSTLY_USED_SOURCE = "Mostly Used" + +internal enum class FilterNetworkAnalytic(val value: String) { + ALL_NETWORKS("All Networks"), + MY_NETWORKS("My Networks"), + SPECIFIC("Specific"), +} + +internal enum class FilterTypeAnalytic(val value: String) { + YIELD("Yield"), + STAKING("Staking"), + ALL_TYPES("All Types"), +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnNetworkFilterModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnNetworkFilterModel.kt new file mode 100644 index 0000000000..793c286f54 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnNetworkFilterModel.kt @@ -0,0 +1,57 @@ +package com.tangem.features.feed.model.earn.filters + +import androidx.compose.runtime.Stable +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent +import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent +import com.tangem.features.feed.model.earn.analytics.FilterNetworkAnalytic +import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkConverter +import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConverter +import com.tangem.features.feed.ui.earn.state.EarnFilterByNetworkBottomSheetContentUM +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class EarnNetworkFilterModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, + paramsContainer: ParamsContainer, +) : Model() { + + private val params = paramsContainer.require() + + private val _state = MutableStateFlow(configureInitialState()) + val state = _state.asStateFlow() + + private fun configureInitialState(): EarnFilterByNetworkBottomSheetContentUM { + return EarnFilterByNetworkBottomSheetContentUM( + networks = params.allFilters + .map { EarnFilterNetworkConverter().convert(it) } + .toPersistentList(), + onOptionClick = ::handleOnOptionClick, + ) + } + + private fun handleOnOptionClick(filter: EarnFilterNetworkUM) { + params.onFilterSelected(EarnFilterNetworkUMConverter().convert(filter)) + val (networkId, filterType) = when (filter) { + is EarnFilterNetworkUM.AllNetworks -> "" to FilterNetworkAnalytic.ALL_NETWORKS + is EarnFilterNetworkUM.MyNetworks -> "" to FilterNetworkAnalytic.MY_NETWORKS + is EarnFilterNetworkUM.Network -> filter.id to FilterNetworkAnalytic.SPECIFIC + } + analyticsEventHandler.send( + EarnAnalyticsEvent.BestOpportunitiesFilterNetworkApplied( + networkId = networkId, + filterType = filterType, + ), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnTypeFilterModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnTypeFilterModel.kt new file mode 100644 index 0000000000..11414b134f --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnTypeFilterModel.kt @@ -0,0 +1,52 @@ +package com.tangem.features.feed.model.earn.filters + +import androidx.compose.runtime.Stable +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.feed.components.earn.EarnTypeFilterComponent +import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent +import com.tangem.features.feed.model.earn.analytics.FilterTypeAnalytic +import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeConverter +import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeUMConverter +import com.tangem.features.feed.ui.earn.state.EarnFilterByTypeBottomSheetContentUM +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class EarnTypeFilterModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, + paramsContainer: ParamsContainer, +) : Model() { + + private val params = paramsContainer.require() + + private val _state = MutableStateFlow(configureInitialState()) + val state = _state.asStateFlow() + + private fun configureInitialState(): EarnFilterByTypeBottomSheetContentUM { + return EarnFilterByTypeBottomSheetContentUM( + selectedOption = EarnFilterTypeConverter().convert(params.selectedFilter), + onOptionClick = ::handleOnOptionClick, + ) + } + + private fun handleOnOptionClick(filter: EarnFilterTypeUM) { + params.onFilterSelected(EarnFilterTypeUMConverter().convert(filter)) + analyticsEventHandler.send( + EarnAnalyticsEvent.BestOpportunitiesFilterTypeApplied( + filterTypeAnalytic = when (filter) { + EarnFilterTypeUM.All -> FilterTypeAnalytic.ALL_TYPES + EarnFilterTypeUM.Staking -> FilterTypeAnalytic.STAKING + EarnFilterTypeUM.YieldMode -> FilterTypeAnalytic.YIELD + }, + ), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterNetworkConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterNetworkConverter.kt new file mode 100644 index 0000000000..62f5348e16 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterNetworkConverter.kt @@ -0,0 +1,31 @@ +package com.tangem.features.feed.model.earn.filters.state + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.domain.earn.model.EarnFilterNetwork +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.utils.converter.Converter + +internal class EarnFilterNetworkConverter : Converter { + + override fun convert(value: EarnFilterNetwork): EarnFilterNetworkUM { + return when (value) { + is EarnFilterNetwork.AllNetworks -> { + EarnFilterNetworkUM.AllNetworks(isSelected = value.isSelected) + } + is EarnFilterNetwork.MyNetworks -> { + EarnFilterNetworkUM.MyNetworks(isSelected = value.isSelected) + } + is EarnFilterNetwork.Specific -> { + EarnFilterNetworkUM.Network( + id = value.id, + text = value.fullName, + symbol = value.symbol, + iconRes = getActiveIconRes(Blockchain.fromNetworkId(value.id)?.id.orEmpty()), + isSelected = value.isSelected, + ) + } + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterNetworkUMConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterNetworkUMConverter.kt new file mode 100644 index 0000000000..5bd1a9c437 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterNetworkUMConverter.kt @@ -0,0 +1,21 @@ +package com.tangem.features.feed.model.earn.filters.state + +import com.tangem.domain.earn.model.EarnFilterNetwork +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.utils.converter.Converter + +internal class EarnFilterNetworkUMConverter : Converter { + + override fun convert(value: EarnFilterNetworkUM): EarnFilterNetwork { + return when (value) { + is EarnFilterNetworkUM.AllNetworks -> EarnFilterNetwork.AllNetworks(isSelected = value.isSelected) + is EarnFilterNetworkUM.MyNetworks -> EarnFilterNetwork.MyNetworks(isSelected = value.isSelected) + is EarnFilterNetworkUM.Network -> EarnFilterNetwork.Specific( + isSelected = value.isSelected, + id = value.id, + symbol = value.symbol, + fullName = value.text, + ) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterTypeConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterTypeConverter.kt new file mode 100644 index 0000000000..0afa48ddd4 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterTypeConverter.kt @@ -0,0 +1,15 @@ +package com.tangem.features.feed.model.earn.filters.state + +import com.tangem.domain.earn.model.EarnFilterType +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM +import com.tangem.utils.converter.Converter + +internal class EarnFilterTypeConverter : Converter { + override fun convert(value: EarnFilterType): EarnFilterTypeUM { + return when (value) { + EarnFilterType.ALL -> EarnFilterTypeUM.All + EarnFilterType.STAKING -> EarnFilterTypeUM.Staking + EarnFilterType.YIELD -> EarnFilterTypeUM.YieldMode + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterTypeUMConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterTypeUMConverter.kt new file mode 100644 index 0000000000..483c27a5d1 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/state/EarnFilterTypeUMConverter.kt @@ -0,0 +1,16 @@ +package com.tangem.features.feed.model.earn.filters.state + +import com.tangem.domain.earn.model.EarnFilterType +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM +import com.tangem.utils.converter.Converter + +internal class EarnFilterTypeUMConverter : Converter { + + override fun convert(value: EarnFilterTypeUM): EarnFilterType { + return when (value) { + EarnFilterTypeUM.All -> EarnFilterType.ALL + EarnFilterTypeUM.Staking -> EarnFilterType.STAKING + EarnFilterTypeUM.YieldMode -> EarnFilterType.YIELD + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt new file mode 100644 index 0000000000..6cec5cd393 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt @@ -0,0 +1,37 @@ +package com.tangem.features.feed.model.earn.state + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.features.feed.model.earn.state.transformers.EarnUMTransformer +import com.tangem.features.feed.ui.earn.state.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@ModelScoped +internal class EarnStateController @Inject constructor() { + + private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState()) + + val uiState: StateFlow get() = mutableUiState.asStateFlow() + + val value: EarnUM get() = uiState.value + + fun update(transformer: EarnUMTransformer) { + mutableUiState.update(function = transformer::transform) + } + + private fun getInitialState(): EarnUM { + return EarnUM( + mostlyUsed = EarnListUM.Loading, + bestOpportunities = EarnBestOpportunitiesUM.Loading, + selectedTypeFilter = EarnFilterTypeUM.All, + selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true), + onBackClick = {}, + onNetworkFilterClick = {}, + onTypeFilterClick = {}, + onSliderScroll = {}, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt new file mode 100644 index 0000000000..9bdb205b49 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class EarnFilterSelectedStateTransformer( + private val filterType: EarnFilterTypeUM, + private val filterNetwork: EarnFilterNetworkUM, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return prevState.copy( + selectedTypeFilter = filterType, + selectedNetworkFilter = filterNetwork, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt new file mode 100644 index 0000000000..a4ae7833a3 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt @@ -0,0 +1,13 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class EarnNetworkFilterSelectedStateTransformer( + private val filter: EarnFilterNetworkUM, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return prevState.copy(selectedNetworkFilter = filter) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnUMTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnUMTransformer.kt new file mode 100644 index 0000000000..0cc4c5b3c3 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnUMTransformer.kt @@ -0,0 +1,7 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal interface EarnUMTransformer { + fun transform(prevState: EarnUM): EarnUM +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateBestOpportunitiesStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateBestOpportunitiesStateTransformer.kt new file mode 100644 index 0000000000..d460f89513 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateBestOpportunitiesStateTransformer.kt @@ -0,0 +1,13 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class UpdateBestOpportunitiesStateTransformer( + private val newState: EarnBestOpportunitiesUM, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return prevState.copy(bestOpportunities = newState) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt new file mode 100644 index 0000000000..d46ac02956 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt @@ -0,0 +1,20 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class UpdateEarnUMInitialStateTransformer( + private val onBackClick: () -> Unit, + private val onNetworkFilterClick: () -> Unit, + private val onTypeFilterClick: () -> Unit, + private val onScroll: () -> Unit, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return prevState.copy( + onBackClick = onBackClick, + onNetworkFilterClick = onNetworkFilterClick, + onTypeFilterClick = onTypeFilterClick, + onSliderScroll = onScroll, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt new file mode 100644 index 0000000000..a3a8317268 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt @@ -0,0 +1,41 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.domain.models.earn.EarnTopToken +import com.tangem.features.feed.model.converter.EarnTokenWithCurrencyToListItemUMConverter +import com.tangem.features.feed.model.earn.analytics.MOSTLY_USED_SOURCE +import com.tangem.features.feed.ui.earn.state.EarnListUM +import com.tangem.features.feed.ui.earn.state.EarnUM +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +internal class UpdateMostlyUsedStateTransformer( + private val earnResult: EarnTopToken?, + private val onItemClick: (EarnTokenWithCurrency, source: String) -> Unit, + private val onRetryClick: () -> Unit, +) : EarnUMTransformer { + + private val converter = EarnTokenWithCurrencyToListItemUMConverter( + onItemClick = { token -> onItemClick(token, MOSTLY_USED_SOURCE) }, + ) + + override fun transform(prevState: EarnUM): EarnUM { + return when (earnResult) { + null -> prevState.copy(mostlyUsed = EarnListUM.Loading) + else -> earnResult.fold( + ifLeft = { prevState.copy(mostlyUsed = EarnListUM.Error(onRetryClicked = onRetryClick)) }, + ifRight = { list -> + val newItems = list + .sortedWith( + compareByDescending { + it.earnToken.apy.toBigDecimalOrNull() ?: BigDecimal.ZERO + }.thenBy { it.earnToken.tokenName }, + ) + .map(converter::convert) + .toPersistentList() + prevState.copy(mostlyUsed = EarnListUM.Content(items = newItems)) + }, + ) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt new file mode 100644 index 0000000000..c241d58af4 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt @@ -0,0 +1,108 @@ +package com.tangem.features.feed.model.earn.statemanager + +import com.tangem.domain.earn.model.EarnTokensBatchingContext +import com.tangem.domain.earn.model.EarnTokensListConfig +import com.tangem.domain.earn.usecase.GetEarnTokensBatchFlowUseCase +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.features.feed.model.converter.EarnTokenWithCurrencyToListItemUMConverter +import com.tangem.features.feed.model.earn.analytics.BEST_OPPORTUNITIES_SOURCE +import com.tangem.features.feed.ui.earn.state.EarnListItemUM +import com.tangem.pagination.BatchAction +import com.tangem.pagination.PaginationStatus +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch + +internal class EarnListBatchFlowManager( + getEarnTokensBatchFlowUseCase: GetEarnTokensBatchFlowUseCase, + private val configProvider: Provider, + private val onItemClick: (EarnTokenWithCurrency, source: String) -> Unit, + private val modelScope: CoroutineScope, + private val dispatchers: CoroutineDispatcherProvider, +) { + private val actionsFlow = MutableSharedFlow>() + private val converter = EarnTokenWithCurrencyToListItemUMConverter( + onItemClick = { onItemClick(it, BEST_OPPORTUNITIES_SOURCE) }, + ) + + private val batchFlow = getEarnTokensBatchFlowUseCase( + context = EarnTokensBatchingContext( + actionsFlow = actionsFlow, + coroutineScope = modelScope, + ), + batchSize = DEFAULT_BATCH_SIZE, + ) + + val uiItems: StateFlow> = + batchFlow.state + .scan(persistentListOf() to -1) { (accItems, lastProcessedBatchIndex), newState -> + if (newState.data.size <= lastProcessedBatchIndex) { + val newItems = newState.data + .flatMap { it.data } + .map(converter::convert) + .toPersistentList() + newItems to newState.data.lastIndex + } else { + val newBatches = newState.data.subList(lastProcessedBatchIndex + 1, newState.data.size) + val newItems = newBatches + .flatMap { it.data } + .map(converter::convert) + accItems.addAll(newItems) to newState.data.lastIndex + } + } + .map { (items, _) -> items } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = persistentListOf(), + ) + + val initialLoadingError: StateFlow = batchFlow.state + .map { state -> + val status = state.status + if (status is PaginationStatus.InitialLoadingError) { + status.throwable + } else { + null + } + } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + + val paginationStatus: StateFlow>> = batchFlow.state + .map { it.status } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = PaginationStatus.InitialLoading, + ) + + fun reload() { + modelScope.launch(dispatchers.default) { + actionsFlow.emit( + BatchAction.Reload(requestParams = configProvider()), + ) + } + } + + fun loadMore() { + modelScope.launch(dispatchers.default) { + actionsFlow.emit(BatchAction.LoadMore()) + } + } + + private companion object { + private const val DEFAULT_BATCH_SIZE = 20 + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListStateManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListStateManager.kt new file mode 100644 index 0000000000..4b53e07f06 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListStateManager.kt @@ -0,0 +1,28 @@ +package com.tangem.features.feed.model.earn.statemanager + +import com.tangem.pagination.PaginationStatus +import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM +import com.tangem.features.feed.ui.earn.state.EarnListItemUM +import kotlinx.collections.immutable.ImmutableList + +@Suppress("LongParameterList") +internal object EarnListStateManager { + + fun calculateState( + items: ImmutableList, + error: Throwable?, + paginationStatus: PaginationStatus<*>, + hasActiveFilters: Boolean, + onRetryClick: () -> Unit, + onLoadMore: () -> Unit, + onClearFiltersClick: () -> Unit, + ): EarnBestOpportunitiesUM = when { + error != null -> EarnBestOpportunitiesUM.Error(onRetryClicked = onRetryClick) + paginationStatus is PaginationStatus.InitialLoading && items.isEmpty() -> + EarnBestOpportunitiesUM.Loading + items.isEmpty() && hasActiveFilters -> + EarnBestOpportunitiesUM.EmptyFiltered(onClearFilterClick = onClearFiltersClick) + items.isEmpty() -> EarnBestOpportunitiesUM.Empty + else -> EarnBestOpportunitiesUM.Content(items = items, onLoadMore = onLoadMore) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt index 8c1774fcf8..fde61b8825 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -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,29 @@ 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.FeedBottomSheetRoute +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent +import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.feed.impl.R +import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent import com.tangem.features.feed.model.feed.state.FeedMarketsBatchFlowManager import com.tangem.features.feed.model.feed.state.FeedStateController -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 +55,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 +90,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 +114,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,11 +165,14 @@ 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, analyticsEventHandler = analyticsEventHandler, ), ) @@ -144,12 +189,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 +197,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 +251,7 @@ internal class FeedComponentModel @Inject constructor( onSliderEndReached = { analyticsEventHandler.send(FeedAnalyticsEvent.NewsCarouselEndReached()) }, + onOpenEarnPageClick = ::handleEarnPageOpenClicked, ), news = NewsUM( content = persistentListOf(), @@ -214,41 +268,14 @@ 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 handleSortTypeClicked(sortByType: SortByTypeUM) { - stateController.update { currentState -> - val updatedCharts = currentState.marketChartConfig.marketCharts.mapValues { (chartSortType, chart) -> - when (chart) { - is MarketChartUM.Content -> { - chart.copy( - sortChartConfig = chart.sortChartConfig.copy( - isSelected = chartSortType == sortByType, - ), - ) - } - else -> chart - } - } - - currentState.copy( - marketChartConfig = currentState.marketChartConfig.copy( - currentSortByType = sortByType, - marketCharts = updatedCharts.toPersistentHashMap(), - ), - ) - } - modelScope.launch(dispatchers.default) { - marketsBatchFlowManager.loadCharts(sortByType.toOrder()) - } - } - private fun startQuotesUpdateTimer() { quotesUpdateJob?.cancel() quotesUpdateJob = modelScope.launch { @@ -274,6 +301,34 @@ internal class FeedComponentModel @Inject constructor( } } + /* start of clicks area */ + private fun handleSortTypeClicked(sortByType: SortByTypeUM) { + stateController.update { currentState -> + val updatedCharts = currentState.marketChartConfig.marketCharts.mapValues { (chartSortType, chart) -> + when (chart) { + is MarketChartUM.Content -> { + chart.copy( + sortChartConfig = chart.sortChartConfig.copy( + isSelected = chartSortType == sortByType, + ), + ) + } + else -> chart + } + } + + currentState.copy( + marketChartConfig = currentState.marketChartConfig.copy( + currentSortByType = sortByType, + marketCharts = updatedCharts.toPersistentHashMap(), + ), + ) + } + modelScope.launch(dispatchers.default) { + marketsBatchFlowManager.loadCharts(sortByType.toOrder()) + } + } + private fun handleMarketItemClicked(item: MarketsListItemUM) { val tokenMarket = marketsBatchFlowManager.getTokenMarketById(item.id) if (tokenMarket != null) { @@ -342,6 +397,41 @@ internal class FeedComponentModel @Inject constructor( params.feedClickIntents.onOpenAllNews() } + private fun handleEarnTokenClick(earnTokenWithCurrency: EarnTokenWithCurrency) { + analyticsEventHandler.send( + EarnAnalyticsEvent.OpportunitySelected( + tokenSymbol = earnTokenWithCurrency.earnToken.tokenSymbol, + blockchain = earnTokenWithCurrency.cryptoCurrency.network.name, + source = AnalyticsParam.ScreensSources.Markets.value, + ), + ) + bottomSheetNavigation.activate( + FeedBottomSheetRoute.AddToPortfolio( + tokenToAdd = AddToPortfolioPreselectedDataComponent.TokenToAdd( + 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, + ), + source = AnalyticsParam.ScreensSources.Markets.value, + ), + ) + } + + private fun handleEarnPageOpenClicked() { + if (state.value.earnListUM is EarnListUM.Content) { + params.feedClickIntents.onOpenEarnPage() + analyticsEventHandler.send(FeedAnalyticsEvent.EarnScreenOpened()) + } + } + /* end of clicks area */ + + /* start of utils area */ private fun SortByTypeUM.toOrder(): TokenMarketListConfig.Order { return when (this) { SortByTypeUM.Rating -> TokenMarketListConfig.Order.ByRating @@ -354,6 +444,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/analytics/FeedAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/analytics/FeedAnalyticsEvent.kt index e40a8503c2..b6279ab0cb 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/analytics/FeedAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/analytics/FeedAnalyticsEvent.kt @@ -83,4 +83,20 @@ internal sealed class FeedAnalyticsEvent( class AllWidgetsLoadError : FeedAnalyticsEvent(event = "All Widgets Load Error") class TokenSearchedClicked : FeedAnalyticsEvent(event = "Token Searched Clicked") + + class EarnScreenOpened : FeedAnalyticsEvent( + event = "Earn Screen Opened", + params = mapOf(SOURCE to AnalyticsParam.ScreensSources.Markets.value), + ) + + data class EarnLoadError( + private val code: Int?, + private val message: String, + ) : FeedAnalyticsEvent( + event = "Earn Load Error", + params = mapOf( + ERROR_CODE to (code ?: IS_NOT_HTTP_ERROR).toString(), + ERROR_MESSAGE to message, + ), + ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/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..fd81cc6428 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt @@ -0,0 +1,90 @@ +package com.tangem.features.feed.model.feed.state.transformers + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.models.earn.EarnError +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.domain.models.earn.EarnTopToken +import com.tangem.features.feed.model.converter.EarnTokenWithCurrencyToListItemUMConverter +import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent +import com.tangem.features.feed.ui.earn.state.EarnListUM +import com.tangem.features.feed.ui.feed.state.FeedListUM +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +internal class UpdateEarnStateTransformer( + private val isEarnEnabled: Boolean, + private val onItemClick: (EarnTokenWithCurrency) -> Unit, + private val onRetryClick: () -> Unit, + private val earnResult: EarnTopToken?, + private val analyticsEventHandler: AnalyticsEventHandler, +) : FeedListUMTransformer { + + private val earnTokenWithCurrencyConverter = EarnTokenWithCurrencyToListItemUMConverter(onItemClick = onItemClick) + + override fun transform(prevState: FeedListUM): FeedListUM { + if (!isEarnEnabled) return handleEmptyState(prevState) + return when (earnResult) { + null -> { + handleEmptyState(prevState) + } + else -> { + earnResult.fold( + ifLeft = { earnError -> + handleErrorState( + currentState = prevState, + result = earnError, + ) + }, + ifRight = { earnTokensWithCurrency -> + handleDataState( + currentState = prevState, + earnTokensWithCurrency = earnTokensWithCurrency, + ) + }, + ) + } + } + } + + private fun handleEmptyState(currentState: FeedListUM): FeedListUM { + return currentState.copy(earnListUM = null) + } + + private fun handleErrorState(currentState: FeedListUM, result: EarnError): FeedListUM { + if (currentState.earnListUM != EarnListUM.Loading) return currentState + val (code, message) = when (result) { + is EarnError.HttpError -> result.code to result.message + is EarnError.NotHttpError -> null to "" + } + analyticsEventHandler.send( + FeedAnalyticsEvent.EarnLoadError( + code = code, + message = message, + ), + ) + return currentState.copy(earnListUM = EarnListUM.Error(onRetryClick)) + } + + private fun handleDataState( + currentState: FeedListUM, + earnTokensWithCurrency: List, + ): FeedListUM { + val newItems = earnTokensWithCurrency + .sortedWith( + compareByDescending { + it.earnToken.apy.toBigDecimalOrNull() ?: BigDecimal.ZERO + }.thenBy { it.earnToken.tokenName }, + ) + .map(earnTokenWithCurrencyConverter::convert) + .toPersistentList() + + val newEarnListUM = when (val earnState = currentState.earnListUM) { + is EarnListUM.Content -> earnState.copy(items = newItems) + else -> EarnListUM.Content(items = newItems) + } + + return currentState.copy( + earnListUM = newEarnListUM, + ) + } +} \ 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..e4af70344b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import arrow.core.Either import arrow.core.getOrElse import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.common.TangemSiteShareUrlBuilder import com.tangem.common.ui.charts.state.MarketChartData import com.tangem.common.ui.charts.state.MarketChartDataProducer import com.tangem.common.ui.charts.state.sorted @@ -11,6 +12,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 +85,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 +250,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( onFirstVisible = {}, onScroll = {}, ), + onShareClick = ::onShareClick, ), ) @@ -662,6 +666,13 @@ internal class MarketsTokenDetailsModel @Inject constructor( } } + private fun onShareClick() { + val tokenId = params.token.id.value + val shareUrl = TangemSiteShareUrlBuilder.shareUrl(tokenId) + shareManager.shareText(shareUrl) + analyticsEventHandler.send(analyticsEventBuilder.shareClicked()) + } + private fun onListedOnClick(exchangesCount: Int) { modelScope.launch { analyticsEventHandler.send(analyticsEventBuilder.exchangesScreenOpened()) 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/model/news/list/NewsListModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/NewsListModel.kt index 9e6153728b..22ba4a7be4 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/NewsListModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/NewsListModel.kt @@ -127,19 +127,17 @@ internal class NewsListModel @Inject constructor( } private fun onCategoryClick(categoryId: Int) { - val newCategoryId = if (state.value.selectedCategoryId == categoryId) { - DEFAULT_ALL_NEWS_CATEGORIES_ID - } else { - categoryId + if (state.value.selectedCategoryId == categoryId) { + return } - if (newCategoryId != DEFAULT_ALL_NEWS_CATEGORIES_ID) { - analyticsEventHandler.send(NewsListAnalyticsEvent.NewsCategoriesClick(newCategoryId)) + if (categoryId != DEFAULT_ALL_NEWS_CATEGORIES_ID) { + analyticsEventHandler.send(NewsListAnalyticsEvent.NewsCategoriesClick(categoryId)) } - selectedCategoryId.value = newCategoryId + selectedCategoryId.value = categoryId _state.update { currentState -> currentState.copy( - selectedCategoryId = newCategoryId, - filters = updateFilterChips(newCategoryId), + selectedCategoryId = categoryId, + filters = updateFilterChips(categoryId), ) } batchFlowManager.reload() diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt index fa5cff5b5e..6b9886c03f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt @@ -15,6 +15,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -55,13 +56,22 @@ internal open class NewsListBatchFlowManager( initialValue = persistentListOf(), ) - val uiItems: StateFlow> - get() = batchFlow.state - .map { batchListState -> - batchListState.data - .flatMap { batch -> batch.data } - .let { articles -> converter.convert(articles) } + val uiItems: StateFlow> = + batchFlow.state + .scan(persistentListOf() to -1) { (accItems, lastProcessedBatchIndex), newState -> + if (newState.data.size <= lastProcessedBatchIndex) { + val newItems = converter.convert(newState.data.flatMap { it.data }) + .toPersistentList() + + newItems to newState.data.lastIndex + } else { + val newBatches = newState.data.subList(lastProcessedBatchIndex + 1, newState.data.size) + val newShortArticles = newBatches.flatMap { it.data } + val newItems = converter.convert(newShortArticles) + accItems.addAll(newItems) to newState.data.lastIndex + } } + .map { (items, _) -> items } .distinctUntilChanged() .stateIn( scope = modelScope, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt index 657d7c312d..0b7845919d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt @@ -1,38 +1,38 @@ 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.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.* +import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onFirstVisible import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.R -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SmallButtonShimmer -import com.tangem.core.ui.components.SpacerW -import com.tangem.core.ui.components.SpacerWMax -import com.tangem.core.ui.components.UnableToLoadData +import com.tangem.core.ui.components.* import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme @@ -40,18 +40,27 @@ import com.tangem.core.ui.res.TangemThemePreview 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.EarnListItemUM -import com.tangem.features.feed.ui.earn.state.EarnListUM -import com.tangem.features.feed.ui.earn.state.EarnUM +import com.tangem.features.feed.ui.earn.state.* import kotlinx.collections.immutable.persistentListOf +private const val EARN_LOAD_MORE_BUFFER = 3 + @Composable internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { val background = LocalMainBottomSheetColor.current.value val density = LocalDensity.current val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() } + val listState = rememberLazyListState() + + if (state.bestOpportunities is EarnBestOpportunitiesUM.Content) { + PaginationHandler( + listState = listState, + state = state.bestOpportunities, + ) + } + LazyColumn( + state = listState, modifier = modifier .fillMaxSize() .background(background), @@ -65,7 +74,10 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { } item(key = "mostly_used_content") { - MostlyUsedContent(state = state.mostlyUsed) + MostlyUsedContent( + state = state.mostlyUsed, + onScroll = state.onSliderScroll, + ) } item(key = "best_opportunities_header") { @@ -79,8 +91,12 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { SpacerH(12.dp) BestOpportunitiesFilters( state = state.bestOpportunities, - selectedNetworkFilter = state.selectedNetworkFilter, - selectedTypeFilter = state.selectedTypeFilter, + selectedNetworkFilterText = when (state.selectedNetworkFilter) { + is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks) + is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks) + is EarnFilterNetworkUM.Network -> TextReference.Str(state.selectedNetworkFilter.text) + }, + selectedTypeFilterText = state.selectedTypeFilterText, onNetworkFilterClick = state.onNetworkFilterClick, onTypeFilterClick = state.onTypeFilterClick, ) @@ -93,41 +109,56 @@ 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, - ) - } +private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) { + AnimatedContent( + targetState = state, + contentKey = { it::class.java }, + ) { 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), + ) { + itemsIndexed( + items = st.items, + key = { _, item -> "${item.tokenName}-${item.network}" }, + ) { index, item -> + val cardModifier = Modifier.conditional( + condition = index == FOURTH_ITEM_INDEX, + modifier = { + onFirstVisible( + minFractionVisible = 0.5f, + callback = onScroll, + ) + }, + ) + MostlyUsedCard( + modifier = cardModifier, + item = item, + onClick = item.onItemClick, + ) + } + } + } + is EarnListUM.Error -> { + Box( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 12.dp, + ), + contentAlignment = Alignment.Center, + ) { + UnableToLoadData(onRetryClick = st.onRetryClicked) + } } } } @@ -157,9 +188,11 @@ private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: verticalAlignment = Alignment.CenterVertically, ) { Text( + modifier = Modifier.weight(weight = 1f, fill = false), text = item.tokenName.resolveReference(), color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle2, + overflow = TextOverflow.Ellipsis, maxLines = 1, ) SpacerW(4.dp) @@ -184,40 +217,27 @@ 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, - ) - } + is EarnBestOpportunitiesUM.Loading -> FilterButtonsShimmer() + else -> FilterButtons( + selectedNetworkFilterText = selectedNetworkFilterText, + selectedTypeFilterText = selectedTypeFilterText, + isEnabled = state is EarnBestOpportunitiesUM.Content || state is EarnBestOpportunitiesUM.EmptyFiltered, + 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 +253,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 +284,7 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnListUM) { } } } - is EarnListUM.Error -> { + is EarnBestOpportunitiesUM.Error -> { item(key = "best_opportunities_error") { SpacerH(12.dp) Box( @@ -275,8 +307,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 +319,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 +330,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,18 +356,93 @@ 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( modifier = modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .padding(start = 20.dp, end = 16.dp), text = title, style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, ) } +@Composable +private fun PaginationHandler(listState: LazyListState, state: EarnBestOpportunitiesUM.Content) { + InfiniteListHandler( + listState = listState, + buffer = EARN_LOAD_MORE_BUFFER, + triggerLoadMoreCheckOnItemsCountChange = true, + onLoadMore = remember(state) { + { + state.onLoadMore() + true + } + }, + ) +} + @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -348,7 +453,7 @@ private fun EarnContentPreview() { LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, ) { EarnContent( - state = EarnUM( + state = previewEarnUM( mostlyUsed = EarnListUM.Content( items = persistentListOf( previewEarnListItemUM(), @@ -359,7 +464,7 @@ private fun EarnContentPreview() { ), ), ), - bestOpportunities = EarnListUM.Content( + bestOpportunities = EarnBestOpportunitiesUM.Content( items = persistentListOf( previewEarnListItemUM( tokenName = "Cosmos Hub", @@ -372,14 +477,8 @@ private fun EarnContentPreview() { network = "Ethereum Network", ), ), + onLoadMore = {}, ), - selectedNetworkFilter = null, - selectedTypeFilter = null, - networkFilters = persistentListOf(), - typeFilters = persistentListOf(), - onBackClick = {}, - onNetworkFilterClick = {}, - onTypeFilterClick = {}, ), ) } @@ -396,16 +495,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 +514,7 @@ private fun EarnContentErrorPreview() { LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, ) { EarnContent( - state = EarnUM( + state = previewEarnUM( mostlyUsed = EarnListUM.Content( items = persistentListOf( previewEarnListItemUM(), @@ -433,14 +525,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 +582,19 @@ private fun previewEarnListItemUM( onItemClick = {}, ) -private const val PLACEHOLDER_ITEMS_COUNT = 8 \ No newline at end of file +private fun previewEarnUM( + mostlyUsed: EarnListUM = EarnListUM.Loading, + bestOpportunities: EarnBestOpportunitiesUM = EarnBestOpportunitiesUM.Loading, +): EarnUM = EarnUM( + mostlyUsed = mostlyUsed, + bestOpportunities = bestOpportunities, + selectedTypeFilter = EarnFilterTypeUM.All, + selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true), + onBackClick = {}, + onNetworkFilterClick = {}, + onTypeFilterClick = {}, + onSliderScroll = {}, +) + +private const val PLACEHOLDER_ITEMS_COUNT = 8 +private const val FOURTH_ITEM_INDEX = 3 \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/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..fc3da1e441 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt @@ -0,0 +1,238 @@ +package com.tangem.features.feed.ui.earn.components + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +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.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.components.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.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.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 = when (item) { + is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks) + is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks) + is EarnFilterNetworkUM.Network -> TextReference.Str(item.text) + }, + checked = item.isSelected, + ) + } + } +} + +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 = { + Image( + modifier = Modifier.size(22.dp), + imageVector = ImageVector.vectorResource(item.iconRes), + contentDescription = item.symbol, + ) + }, + text = { + RowText( + mainText = item.text, + secondText = item.symbol, + 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( + networks = persistentListOf( + EarnFilterNetworkUM.AllNetworks(isSelected = true), + EarnFilterNetworkUM.MyNetworks(isSelected = false), + EarnFilterNetworkUM.Network( + id = "ethereum", + text = "Ethereum", + symbol = "ETH", + iconRes = R.drawable.img_btc_22, + isSelected = false, + ), + EarnFilterNetworkUM.Network( + id = "polygon", + text = "Polygon", + symbol = "MATIC", + iconRes = R.drawable.img_btc_22, + 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..585cf7aa3c --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByNetworkBottomSheetContentUM.kt @@ -0,0 +1,9 @@ +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 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..207fecdc5c --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt @@ -0,0 +1,25 @@ +package com.tangem.features.feed.ui.earn.state + +import androidx.compose.runtime.Immutable + +@Immutable +internal sealed class EarnFilterNetworkUM { + + abstract val isSelected: Boolean + + data class AllNetworks( + override val isSelected: Boolean, + ) : EarnFilterNetworkUM() + + data class MyNetworks( + override val isSelected: Boolean, + ) : EarnFilterNetworkUM() + + data class Network( + override val isSelected: Boolean, + val id: String, + val text: String, + val symbol: String, + val iconRes: Int, + ) : EarnFilterNetworkUM() +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/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..c4506586bc 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt @@ -12,6 +12,18 @@ 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, + val onLoadMore: () -> Unit, + ) : 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..a11f66f48e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt @@ -2,25 +2,19 @@ package com.tangem.features.feed.ui.earn.state import androidx.compose.runtime.Immutable 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 onBackClick: () -> Unit, val onNetworkFilterClick: () -> Unit, val onTypeFilterClick: () -> Unit, -) + val onSliderScroll: () -> 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 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..fb70686941 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt @@ -0,0 +1,82 @@ +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, + contentKey = { it::class.java }, + ) { earnListUM -> + when (earnListUM) { + is EarnListUM.Content -> EarnContentBlock(items = earnListUM.items) + is EarnListUM.Error -> EarnErrorBlock(onRetryClick = earnListUM.onRetryClicked) + EarnListUM.Loading -> EarnListPlaceholder(placeholderCount = PLACEHOLDER_ITEM_COUNT) + } + } + } + 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/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/CreatedTimeFormatter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/CreatedTimeFormatter.kt index b1052994f0..ef92d71b3c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/CreatedTimeFormatter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/CreatedTimeFormatter.kt @@ -10,10 +10,13 @@ import com.tangem.utils.StringsSigns import org.joda.time.DateTime internal fun mapFormattedDate(createdAt: String): TextReference { - val formattedDate = getFormattedDate( - createdAt = createdAt, - now = DateTime.now(), - ) + val formattedDate = runCatching { + getFormattedDate( + createdAt = createdAt, + now = DateTime.now(), + ) + }.getOrElse { FormattedDate.FullDate("") } + return when (formattedDate) { is FormattedDate.FullDate -> TextReference.Str(value = formattedDate.date) is FormattedDate.HoursAgo -> TextReference.PluralRes( 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..7cb91f3880 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -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, @@ -175,6 +171,7 @@ internal class HomeModel @Inject constructor( scanCardProcessor.scan( analyticsSource = analyticsSource, + shouldCheckIsAlreadyActivated = true, onProgressStateChange = { showProgress -> if (!showProgress) { delay(HIDE_PROGRESS_DELAY) @@ -233,7 +230,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 +238,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/createhardwarewallet/CreateHardwareWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt index 5d32c8952b..49c8bc6351 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createhardwarewallet/CreateHardwareWalletModel.kt @@ -104,6 +104,7 @@ internal class CreateHardwareWalletModel @Inject constructor( scanCardProcessor.scan( analyticsSource = analyticsSource, + shouldCheckIsAlreadyActivated = true, onProgressStateChange = { showProgress -> if (!showProgress) { delay(HIDE_PROGRESS_DELAY) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/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/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt index c7252d49c4..3d8d7bc0bd 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/upgradewallet/UpgradeWalletModel.kt @@ -115,7 +115,7 @@ internal class UpgradeWalletModel @Inject constructor( ) tangemSdkManager - .scanProduct() + .scanProduct(shouldCheckIsAlreadyActivated = true) .doOnSuccess { scanResponse -> checkIsWalletSuitableToBeUsedAsUpgrade(scanResponse = scanResponse) { delay(DELAY_SDK_DIALOG_CLOSE) diff --git a/features/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/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index 1ce00a7b83..d741fc5ed0 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.managetokens.model import arrow.core.getOrElse +import com.tangem.common.core.TangemSdkError import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -8,6 +9,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.managetokens.CreateCryptoCurrencyUseCase import com.tangem.domain.managetokens.FindTokenUseCase @@ -146,7 +148,7 @@ internal class CustomTokenFormModel @Inject constructor( is CustomCurrencyValidator.Status.Validating, -> Unit is CustomCurrencyValidator.Status.SearchingToken -> updateStateWithProgress() - is CustomCurrencyValidator.Status.UnexpectedException -> showErrorDialog() + is CustomCurrencyValidator.Status.UnexpectedException -> showErrorDialog(validatorState.cause) is CustomCurrencyValidator.Status.FormValidationException -> updateStateWithExceptions( exceptions = validatorState.exceptions, ) @@ -236,9 +238,17 @@ internal class CustomTokenFormModel @Inject constructor( } } - private fun showErrorDialog() { + private fun showErrorDialog(throwable: Throwable) { + Timber.e(throwable) + val message = when (throwable) { + is TangemSdkError -> resourceReference( + R.string.generic_error_code, + wrappedList(throwable.code.toString()), + ) + else -> resourceReference(R.string.common_unknown_error) + } val dialog = DialogMessage( - message = resourceReference(R.string.common_unknown_error), + message = message, ) messageSender.send(dialog) @@ -341,8 +351,17 @@ internal class CustomTokenFormModel @Inject constructor( ) { val currency = createdCurrency if (currency == null) { - Timber.e("Trying to add currency without validation") - showErrorDialog() + showErrorDialog(IllegalStateException("Trying to add currency without validation")) + return@resource + } + + useCasesFacade.derivePublicKeysUseCase(listOf(currency)).getOrElse { + showErrorDialog(IllegalStateException("Failed to derive public keys")) + return@resource + } + + useCasesFacade.addCryptoCurrenciesUseCase(currency).getOrElse { throwable -> + showErrorDialog(throwable) return@resource } @@ -353,18 +372,6 @@ internal class CustomTokenFormModel @Inject constructor( ) analyticsEventHandler.send(event) - useCasesFacade.derivePublicKeysUseCase(listOf(currency)).getOrElse { - Timber.e(it, "Failed to derive public keys") - showErrorDialog() - return@resource - } - - useCasesFacade.addCryptoCurrenciesUseCase(currency).getOrElse { - Timber.e(it, "Failed to add currency") - showErrorDialog() - return@resource - } - params.onCurrencyAdded(currency) } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt index 2cbfe3dc7a..7aa77b2a2b 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt @@ -214,9 +214,10 @@ internal class CustomTokenSelectorModel @Inject constructor( val accounts = singleAccountStatusListSupplier(mode.userWalletId) .first().accountStatuses - val accountStatus = accounts.find { - when (it) { - is AccountStatus.CryptoPortfolio -> it.sameNodeAndNotMain() + val accountStatus = accounts.find { account -> + when (account) { + is AccountStatus.CryptoPortfolio -> account.sameNodeAndNotMain() + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 74ee2da6c5..7c5611d12e 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -298,7 +298,7 @@ internal class ManageTokensModel @Inject constructor( val networks = currenciesToAdd.values .flatten() .toSet() - .associate { it.backendId to null } + .associate { network -> network.backendId to network.derivationPath.value } val needToInteractWithColdWallet = useCasesFacade.needColdWalletInteraction(networks) state.update { state -> diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt index 50267c156a..e2aaa97cca 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt @@ -222,7 +222,7 @@ internal class OnboardingManageTokensModel @Inject constructor( val network = currenciesToAdd.values .flatten() .toSet() - .associate { it.backendId to null } + .associate { network -> network.backendId to network.derivationPath.value } val showTangemIcon = useCasesFacade.needColdWalletInteraction(network = network) state.update { state -> state.copy( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt index 51a46bd0e0..d446dd0141 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt @@ -44,7 +44,11 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( either { val accountId = getAccountId(currency) - manageCryptoCurrenciesUseCase(accountId = accountId, add = currency).bind() + manageCryptoCurrenciesUseCase( + accountId = accountId, + add = currency, + skipDerivationErrors = false, + ).bind() } } else { addCryptoCurrenciesUseCase.invoke(userWalletId = userWalletId, currency = currency) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt index c3fe0bf606..e692352aad 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt @@ -12,6 +12,7 @@ import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.managetokens.model.ManageTokensListConfig import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.repository.CustomTokensRepository +import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId @@ -88,7 +89,7 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( val account = singleAccountSupplier.getSyncOrNull( params = SingleAccountProducer.Params(accountId = mode.accountId), - ) + ) as? Account.CryptoPortfolio ?: return IllegalStateException("Account not found").left() (account.cryptoCurrencies + added - removed).any { @@ -123,7 +124,7 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( } } - suspend fun needColdWalletInteraction(network: Map): Boolean = when (mode) { + suspend fun needColdWalletInteraction(network: Map): Boolean = when (mode) { is ManageTokensMode.Account -> coldWalletAndHasMissedDerivationsUseCase.invoke( userWalletId = mode.accountId.userWalletId, networksWithDerivationPath = network, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt index 6b0fd6a169..71b3634b3d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -151,6 +151,8 @@ internal class AddToPortfolioModel @Inject constructor( allRequireForAdd.first() // line of navigation to AddToken screen is finished; cancel the job, select a new root screen firstPartOfNavigation.cancel() + + analyticsEventHandler.send(event = eventBuilder.popupToConfirm()) navigation.replaceAll(AddToPortfolioRoutes.AddToken) var middleNavigationJob: Job? = null @@ -315,6 +317,7 @@ internal class AddToPortfolioModel @Inject constructor( ): CryptoCurrency? { val accountIndex = when (account.account) { is AccountStatus.CryptoPortfolio -> account.account.account.derivationIndex + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } return getTokenMarketCryptoCurrency( userWalletId = userWallet.walletId, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt index 7f8214d8fa..be45fedb3a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt @@ -52,7 +52,7 @@ internal class AddTokenModel @Inject constructor( flow = params.selectedNetwork.distinctUntilChanged(), flow2 = params.selectedPortfolio.distinctUntilChanged(), transform = { selectedNetwork, selectedPortfolio -> - addTokenJob.cancel() + addTokenJob.join() val isTangemIconVisible = needColdWalletInteraction(selectedNetwork, selectedPortfolio) uiBuilder.updateContent( selectedPortfolio = selectedPortfolio, @@ -88,6 +88,7 @@ internal class AddTokenModel @Inject constructor( val blockchainNames = listOf(selectedNetwork.selectedNetwork) .mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name } analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames)) + analyticsEventHandler.send(analyticsEventBuilder.addButtonClick()) manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency) .onLeft { error -> @@ -110,6 +111,11 @@ internal class AddTokenModel @Inject constructor( } is Account.Payment -> TODO("[REDACTED_JIRA]") } + + analyticsEventHandler.send( + event = analyticsEventBuilder.tokenAdded(status.status.currency.network.name), + ) + params.callbacks.onTokenAdded(status.status) } uiState.value = um.toggleProgress(false) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenUiBuilder.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenUiBuilder.kt index 107c7a8c3b..ec88d88216 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenUiBuilder.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenUiBuilder.kt @@ -13,7 +13,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.iconResId import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.AccountStatus.* import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.add.api.SelectedNetwork import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio @@ -36,7 +36,7 @@ internal class AddTokenUiBuilder @Inject constructor( } private fun createPortfolio(selectedPortfolio: SelectedPortfolio): PortfolioSelectUM { - val accountIcon: AccountIconUM.CryptoPortfolio? + val accountIcon: AccountIconUM? val portfolioName: TextReference when (selectedPortfolio.isAccountMode) { false -> { @@ -47,7 +47,8 @@ internal class AddTokenUiBuilder @Inject constructor( val accountStatus = selectedPortfolio.account.account portfolioName = accountStatus.account.accountName.toUM().value accountIcon = when (accountStatus) { - is AccountStatus.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon) + is CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon) + is Payment -> AccountIconUM.Payment } } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt index a61f7cd842..f6d5e737b6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt @@ -66,7 +66,7 @@ class CheckCurrencyUnsupportedDelegate @Inject constructor( formatArgs = wrappedList(unsupportedState.networkName), ) is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, + id = R.string.alert_manage_tokens_unsupported_blockchain_by_card_message, formatArgs = wrappedList(unsupportedState.networkName), ) }, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt index ac6125d3c0..2658bb0659 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt @@ -16,28 +16,64 @@ internal class PortfolioAnalyticsEvent( fun addToPortfolioClicked() = PortfolioAnalyticsEvent( event = "Button - Add To Portfolio", - params = mapOf( - "Token" to token.symbol, - ), + params = buildMap { + put("Token", token.symbol) + if (source != null) put("Source", source) + }, ) fun popupToChooseAccount() = PortfolioAnalyticsEvent( event = "Choose Account Opened", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun popupToConfirm() = PortfolioAnalyticsEvent( + event = "Add Token Screen Opened", + params = buildMap { + if (source != null) put("Source", source) + }, ) fun addToNotMainAccount() = PortfolioAnalyticsEvent( event = "Button - Add To Account", + params = buildMap { + if (source != null) put("Source", source) + }, ) - fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(event = "Wallet Selected") + fun addButtonClick() = PortfolioAnalyticsEvent( + event = "Button - Add Token", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent( + event = "Wallet Selected", + params = buildMap { + if (source != null) put("Source", source) + }, + ) fun addToPortfolioContinue(blockchainNames: List) = PortfolioAnalyticsEvent( event = "Token Network Selected", - params = mapOf( - "Count" to blockchainNames.size.toString(), - "Token" to token.symbol, - "blockchain" to blockchainNames.joinToString(separator = ", "), - ), + params = buildMap { + put("Count", blockchainNames.size.toString()) + put("Token", token.symbol) + put("blockchain", blockchainNames.joinToString(separator = ", ")) + if (source != null) put("Source", source) + }, + ) + + fun tokenAdded(blockchainName: String) = PortfolioAnalyticsEvent( + event = "Token Added", + params = buildMap { + put("Token", token.symbol) + put("Blockchain", blockchainName) + if (source != null) put("Source", source) + }, ) fun quickActionClick(actionUM: TokenActionsBSContentUM.Action, blockchainName: String) = @@ -65,10 +101,16 @@ internal class PortfolioAnalyticsEvent( TokenActionsBSContentUM.Action.Stake -> "Popup Get token - Button Stake" else -> "error" }, + params = buildMap { + if (source != null) put("Source", source) + }, ) fun getTokenLater() = PortfolioAnalyticsEvent( event = "Popup Get token - Button Later", + params = buildMap { + if (source != null) put("Source", source) + }, ) } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt index 286e1dc18e..17acbaf94a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt @@ -203,6 +203,7 @@ internal class NewMarketsPortfolioDelegate @AssistedInject constructor( val currencyId = status.currency.id.rawCurrencyId ?: return@filter false getTokenIdIfL2Network(currencyId.value) == currencyRawId.value } + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } return accountStatuses.map { accountStatus -> AccountWithAdded( diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/NFTFeatureToggles.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/NFTFeatureToggles.kt deleted file mode 100644 index 36689ae915..0000000000 --- a/features/nft/api/src/main/kotlin/com/tangem/features/nft/NFTFeatureToggles.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.features.nft - -interface NFTFeatureToggles { - - val isNFTMediaContentEnabled: Boolean -} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/DefaultNFTFeatureToggles.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/DefaultNFTFeatureToggles.kt deleted file mode 100644 index 6ee99738ee..0000000000 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/DefaultNFTFeatureToggles.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.features.nft - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager - -internal class DefaultNFTFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : NFTFeatureToggles { - - override val isNFTMediaContentEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "NFT_MEDIA_CONTENT_ENABLED") -} \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt index 1b3221badd..9d85522a2c 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/di/NFTFeatureModule.kt @@ -1,9 +1,6 @@ package com.tangem.features.nft.di -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.decompose.model.Model -import com.tangem.features.nft.DefaultNFTFeatureToggles -import com.tangem.features.nft.NFTFeatureToggles import com.tangem.features.nft.collections.model.NFTCollectionsModel import com.tangem.features.nft.common.DefaultNFTComponent import com.tangem.features.nft.component.* @@ -18,7 +15,6 @@ import com.tangem.features.nft.receive.model.NFTReceiveModel import com.tangem.features.nft.traits.model.NFTAssetTraitsModel import dagger.Binds import dagger.Module -import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import dagger.multibindings.ClassKey @@ -27,18 +23,7 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -internal object NFTFeatureModule { - - @Provides - @Singleton - fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): NFTFeatureToggles { - return DefaultNFTFeatureToggles(featureTogglesManager) - } -} - -@Module -@InstallIn(SingletonComponent::class) -internal interface NFTFeatureModuleBinds { +internal interface NFTFeatureModule { @Binds @Singleton fun bindNFTDetailsInfoComponentFactory( 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/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt index 8779ab378a..a5fda3ab83 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt @@ -28,6 +28,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.security.DisableAutofillEffect import com.tangem.features.onboarding.v2.impl.R import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.utils.InvalidWordsColorTransformation @@ -53,6 +54,8 @@ internal fun MultiWalletSeedPhraseImport(state: MultiWalletSeedPhraseUM.Import, ) } + DisableAutofillEffect() + Box(modifier.fillMaxSize()) { Column( modifier = Modifier diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt index 03d7c218c2..636c9f4f32 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt @@ -43,8 +43,8 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor( componentScope.launch { val cardInfo = getWalletMetaInfoUseCase(params.scanResponse).getOrNull() ?: return@launch - val userWalletId = cardInfo.userWalletId ?: return@launch - val visaCustomerId = getTangemPayCustomerIdUseCase(userWalletId).getOrNull() + val userWalletId = cardInfo.userWalletId + val visaCustomerId = userWalletId?.let { id -> getTangemPayCustomerIdUseCase(id).getOrNull() } sendFeedbackEmailUseCase( if (params.scanResponse.card.isVisa && !visaCustomerId.isNullOrEmpty()) { FeedbackEmailType.Visa.Activation(walletMetaInfo = cardInfo, customerId = visaCustomerId) diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index 1e3239f200..f0a6cb16c5 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -15,9 +15,11 @@ dependencies { /** Project - API */ implementation(projects.features.account.api) implementation(projects.features.onramp.api) + implementation(projects.features.swap.api) implementation(projects.features.swap.domain) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) + implementation(projects.features.feed.api) /** Project - Core */ implementation(projects.core.analytics) @@ -30,6 +32,8 @@ dependencies { /** Project - Common */ implementation(projects.common.routing) implementation(projects.common.ui) + implementation(projects.common.uiMarkets) + implementation(projects.common.uiCharts) /** Project - Domain */ implementation(projects.domain.appCurrency) @@ -51,6 +55,7 @@ dependencies { implementation(projects.domain.appTheme) implementation(projects.domain.appTheme.models) implementation(projects.data.common) + implementation(projects.domain.markets) /** DI */ implementation(deps.hilt.android) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoPortfolioDataLoader.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoPortfolioDataLoader.kt index 961019b158..d49f018ad2 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoPortfolioDataLoader.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoPortfolioDataLoader.kt @@ -45,6 +45,7 @@ internal class HotCryptoPortfolioDataLoader @Inject constructor( val accountsWithHotCrypto = walletAccounts.accountStatuses.map { accountStatus -> val account: AccountStatus.CryptoPortfolio = when (accountStatus) { is AccountStatus.CryptoPortfolio -> accountStatus + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } val addedHotCrypto = mapOfAddedCurrencies[account.account].orEmpty() HotCryptoPortfolioData.Account( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddTokenUiBuilder.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddTokenUiBuilder.kt index a7b415212a..ac1f747c49 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddTokenUiBuilder.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/entity/OnrampAddTokenUiBuilder.kt @@ -37,7 +37,7 @@ internal class OnrampAddTokenUiBuilder @Inject constructor( } private suspend fun createPortfolio(tokenToAdd: AddHotCryptoData): PortfolioSelectUM { - val accountIcon: AccountIconUM.CryptoPortfolio? + val accountIcon: AccountIconUM? val portfolioName: TextReference val isAccountMode = isAccountsModeEnabledUseCase.invokeSync() when (isAccountMode) { @@ -50,6 +50,7 @@ internal class OnrampAddTokenUiBuilder @Inject constructor( portfolioName = accountStatus.account.accountName.toUM().value accountIcon = when (accountStatus) { is AccountStatus.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon) + is AccountStatus.Payment -> AccountIconUM.Payment } } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt index d9b66c559d..ee2dc2dd5b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt @@ -5,13 +5,20 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent import com.tangem.features.onramp.component.SwapSelectTokensComponent import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent +import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute import com.tangem.features.onramp.swap.model.SwapSelectTokensModel import com.tangem.features.onramp.swap.ui.SwapSelectTokens import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent @@ -25,6 +32,7 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor( tokenListComponentFactory: OnrampTokenListComponent.Factory, availableSwapPairsComponentFactory: AvailableSwapPairsComponent.Factory, analyticsEventHandler: AnalyticsEventHandler, + private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, @Assisted private val appComponentContext: AppComponentContext, @Assisted private val params: SwapSelectTokensComponent.Params, ) : AppComponentContext by appComponentContext, SwapSelectTokensComponent { @@ -49,15 +57,36 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor( ), ) + private val bottomSheetSlot = childSlot( + source = selectToTokenListComponent.bottomSheetNavigation, + serializer = AddToPortfolioRoute.serializer(), + key = "add_to_portfolio_bottom_sheet", + handleBackButton = false, + childFactory = { _, context -> bottomSheetChild(context) }, + ) + init { analyticsEventHandler.send(event = MainScreenAnalyticsEvent.SwapScreenOpened()) } + @Suppress("UnsafeCallOnNullableType") + private fun bottomSheetChild(componentContext: ComponentContext): ComposableBottomSheetComponent { + return addToPortfolioComponentFactory.create( + context = childByContext(componentContext), + params = AddToPortfolioComponent.Params( + addToPortfolioManager = selectToTokenListComponent.addToPortfolioManager!!, + callback = selectToTokenListComponent.addToPortfolioCallback, + shouldSkipTokenActionsScreen = true, + ), + ) + } + @Composable override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() val fromTokensState by selectFromTokenListComponent.uiState.collectAsStateWithLifecycle() val toTokensState by selectToTokenListComponent.uiState.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() SwapSelectTokens( state = state, @@ -67,6 +96,8 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor( selectToTokenListState = toTokensState, modifier = modifier, ) + + bottomSheet.child?.instance?.BottomSheet() } @AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt index cb87a50dc2..bae0b5ba51 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt @@ -1,11 +1,15 @@ package com.tangem.features.onramp.swap.availablepairs import androidx.compose.runtime.Stable +import com.arkivanov.decompose.router.slot.SlotNavigation import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.decompose.ComposableListContentComponent import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager +import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute import com.tangem.features.onramp.tokenlist.entity.TokenListUM import kotlinx.coroutines.flow.StateFlow @@ -13,6 +17,10 @@ import kotlinx.coroutines.flow.StateFlow @Stable internal interface AvailableSwapPairsComponent : ComposableListContentComponent { + val bottomSheetNavigation: SlotNavigation + val addToPortfolioManager: AddToPortfolioManager? + val addToPortfolioCallback: AddToPortfolioComponent.Callback + /** Component factory */ interface Factory : ComponentFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt index eea5740b23..27e528cd04 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt @@ -3,11 +3,15 @@ package com.tangem.features.onramp.swap.availablepairs import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier +import com.arkivanov.decompose.router.slot.SlotNavigation import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager +import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute import com.tangem.features.onramp.swap.availablepairs.model.AvailableSwapPairsModel import com.tangem.features.onramp.tokenlist.entity.TokenListUM -import com.tangem.features.onramp.tokenlist.ui.onrampTokenList +import com.tangem.features.onramp.tokenlist.ui.onrampSwapTokenList import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -21,11 +25,15 @@ internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor( private val model: AvailableSwapPairsModel = getOrCreateModel(params) + override val bottomSheetNavigation: SlotNavigation get() = model.bottomSheetNavigation + override val addToPortfolioManager: AddToPortfolioManager? get() = model.addToPortfolioManager + override val addToPortfolioCallback: AddToPortfolioComponent.Callback get() = model.addToPortfolioCallback + override val uiState: StateFlow get() = model.state override fun LazyListScope.content(uiState: TokenListUM, modifier: Modifier) { - onrampTokenList(state = uiState) + onrampSwapTokenList(state = uiState) } @AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt index 9489858928..8040f59244 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt @@ -23,7 +23,9 @@ internal class LoadingAccountTokenItemConverter( ).convert(TotalFiatBalance.Failed), isExpanded = true, isCollapsable = false, - tokens = currencies.flattenCurrencies().map(LoadingTokenListItemConverter::convert).toPersistentList(), + tokens = currencies.flattenCurrencies() + .map { LoadingTokenListItemConverter.convert(it.currency) } + .toPersistentList(), ) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt index 55e336791d..68932d0f66 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingTokenListItemConverter.kt @@ -3,6 +3,7 @@ package com.tangem.features.onramp.swap.availablepairs.entity.converters import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.utils.converter.Converter @@ -11,12 +12,12 @@ import com.tangem.utils.converter.Converter * [REDACTED_AUTHOR] */ -internal object LoadingTokenListItemConverter : Converter { +internal object LoadingTokenListItemConverter : Converter { - override fun convert(value: CryptoCurrencyStatus): TokensListItemUM.Token { + override fun convert(value: CryptoCurrency): TokensListItemUM.Token { return TokensListItemUM.Token( state = TokenItemState.Loading( - id = value.currency.id.value, + id = value.id.value, iconState = CurrencyIconState.Loading, titleState = TokenItemState.TitleState.Loading, subtitleState = TokenItemState.SubtitleState.Loading, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt index ccf527232b..02b9d632dc 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt @@ -18,7 +18,9 @@ internal class SetLoadingTokenItemsTransformer( override fun transform(prevState: TokenListUM): TokenListUM { return prevState.copy( - availableItems = LoadingTokenListItemConverter.convertList(input = statuses).toImmutableList(), + availableItems = LoadingTokenListItemConverter.convertList( + input = statuses.map(CryptoCurrencyStatus::currency), + ).toImmutableList(), unavailableItems = persistentListOf(), warning = null, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt index 9ee61d4ba9..ac87e348b8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt @@ -26,6 +26,8 @@ internal class SetNoAvailablePairsTransformerV2( .createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText) override fun transform(prevState: TokenListUM): TokenListUM { + val totalTokensCount = accountList.values.sumOf { it.size } + return prevState.copy( availableItems = persistentListOf(), unavailableItems = persistentListOf(), @@ -45,6 +47,7 @@ internal class SetNoAvailablePairsTransformerV2( .toPersistentList(), ) }.toPersistentList(), + totalTokensCount = totalTokensCount, ) } else { TokenListUMData.TokenList( @@ -52,6 +55,7 @@ internal class SetNoAvailablePairsTransformerV2( unavailableConverter.convertList(cryptoCurrencies) .map(TokensListItemUM::Token) }.toPersistentList(), + totalTokensCount = totalTokensCount, ) }, isBalanceHidden = isBalanceHidden, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt new file mode 100644 index 0000000000..5df10fda71 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt @@ -0,0 +1,252 @@ +package com.tangem.features.onramp.swap.availablepairs.market + +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.* +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.features.onramp.swap.availablepairs.market.converter.SwapMarketsTokenItemConverter +import com.tangem.pagination.Batch +import com.tangem.pagination.BatchAction +import com.tangem.pagination.PaginationStatus +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +@Suppress("LongParameterList") +internal class SwapMarketsListBatchFlowManager( + getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, + private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType, + private val order: TokenMarketListConfig.Order, + private val currentAppCurrency: Provider, + private val currentSearchText: Provider, + private val modelScope: CoroutineScope, + private val dispatchers: CoroutineDispatcherProvider, +) { + private val actionsFlow = MutableSharedFlow>() + private val updateStateJob = JobHolder() + + private val batchFlow = getMarketsTokenListFlowUseCase( + batchingContext = TokenListBatchingContext( + actionsFlow = actionsFlow, + coroutineScope = modelScope, + ), + batchFlowType = batchFlowType, + ) + + private val resultBatches = MutableStateFlow(ResultBatches()) + private val uiBatches = resultBatches.map { it.uiBatches } + + val uiItems: StateFlow> + get() = uiBatches + .map { batches -> + batches.asSequence() + .map { it.data } + .flatten() + .toImmutableList() + } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = persistentListOf(), + ) + + val isInInitialLoadingErrorState = batchFlow.state + .map { it.status is PaginationStatus.InitialLoadingError } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + + val isSearchNotFoundState = batchFlow.state + .map { batchListState -> + currentSearchText().isNullOrEmpty().not() && + batchListState.status is PaginationStatus.EndOfPagination && + batchListState.data.isEmpty() + } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + + val totalCount: StateFlow = batchFlow.state + .map { it.totalCount } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + + init { + batchFlow.state + .map { it.data } + .distinctUntilChanged { a, b -> + a.size == b.size && + a.map { it.key } == b.map { it.key } && + a.map { it.data }.flatten() == b.map { it.data }.flatten() + } + .onEach { + coroutineScope { + launch { + updateState(it) + }.saveIn(updateStateJob) + } + } + .flowOn(dispatchers.default) + .launchIn(modelScope) + } + + private suspend fun updateState(newList: List>>, forceUpdate: Boolean = false) = + withContext(dispatchers.default) { + resultBatches.update { resultBatches -> + val items = resultBatches.uiBatches + val previousList = resultBatches.processedItems + + val converter = SwapMarketsTokenItemConverter(appCurrency = currentAppCurrency()) + + if (newList.isEmpty()) { + return@update ResultBatches(processedItems = emptyList()) + } + + val isInitialLoading = + forceUpdate || previousList.isNullOrEmpty() || newList.first().key != previousList.first().key + + val outItems = if (isInitialLoading) { + newList.map { batch -> + Batch( + key = batch.key, + data = converter.convertList(batch.data), + ) + } + } else { + if (previousList.size != newList.size) { + val keysToAdd = newList.map { it.key }.subtract(previousList.map { it.key }.toSet()) + val newBatches = newList.filter { keysToAdd.contains(it.key) } + + items + newBatches.map { batch -> + Batch( + key = batch.key, + data = converter.convertList(batch.data), + ) + } + } else { + items.mapIndexed { batchIndex, batch -> + val prevBatch = previousList[batchIndex] + val newBatch = newList[batchIndex] + if (prevBatch == newBatch) return@mapIndexed batch + + Batch( + key = batch.key, + data = batch.data.mapIndexed { index, marketsListItemUM -> + val prevItem = prevBatch.data.getOrNull(index) + val newItem = newBatch.data.getOrNull(index) + if (prevItem != null && newItem != null) { + converter.update(prevItem, marketsListItemUM, newItem) + } else { + newItem?.let { converter.convert(it) } ?: marketsListItemUM + } + }, + ) + } + } + } + + currentCoroutineContext().ensureActive() + + ResultBatches( + uiBatches = outItems, + processedItems = newList, + ) + } + } + + fun reload(searchText: String? = null) { + modelScope.launch { + resultBatches.value = ResultBatches() + actionsFlow.emit( + BatchAction.Reload( + requestParams = TokenMarketListConfig( + fiatPriceCurrency = currentAppCurrency().code, + searchText = if (currentSearchText() == null) { + null + } else { + searchText ?: currentSearchText() + }, + priceChangeInterval = TokenMarketListConfig.Interval.H24, + order = order, + shouldNetworks = true, + ), + ), + ) + } + } + + fun loadMore() { + modelScope.launch { + actionsFlow.emit(BatchAction.LoadMore()) + } + } + + fun loadCharts(batchKeys: Set) { + if (batchKeys.isEmpty()) return + + modelScope.launch { + val currentData = batchFlow.state.value.data + val alreadyLoadedChartsBatchKeys = currentData + .filter { batch -> + val first = batch.data.firstOrNull() ?: return@filter false + first.tokenCharts.h24 != null + } + .map { it.key } + .toSet() + + val batchesKeysToLoad = batchKeys.minus(alreadyLoadedChartsBatchKeys) + + if (batchesKeysToLoad.isNotEmpty()) { + actionsFlow.emit( + BatchAction.UpdateBatches( + keys = batchesKeysToLoad, + updateRequest = TokenMarketUpdateRequest.UpdateChart( + interval = TokenMarketListConfig.Interval.H24, + currency = currentAppCurrency().code, + ), + async = true, + operationId = batchesKeysToLoad.toString() + "h24", + ), + ) + } + } + } + + fun getTokenMarketById(id: CryptoCurrency.RawID): TokenMarket? { + return batchFlow.state.value.data + .asSequence() + .flatMap { it.data } + .firstOrNull { it.id == id } + } + + fun getBatchKeysByItemIds(ids: List): Set { + val currentData = batchFlow.state.value.data + + return currentData + .filter { d -> d.data.any { ids.contains(it.id) } } + .map { it.key } + .toSet() + } + + private data class ResultBatches( + val uiBatches: List>> = emptyList(), + val processedItems: List>>? = null, + ) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/converter/SwapMarketsTokenItemConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/converter/SwapMarketsTokenItemConverter.kt new file mode 100644 index 0000000000..f76ce2af6d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/converter/SwapMarketsTokenItemConverter.kt @@ -0,0 +1,155 @@ +package com.tangem.features.onramp.swap.availablepairs.market.converter + +import com.tangem.common.ui.charts.state.MarketChartData +import com.tangem.common.ui.charts.state.MarketChartRawData +import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter +import com.tangem.common.ui.charts.state.sorted +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.ui.R +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.compact +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.core.ui.format.bigdecimal.price +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarket +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal +import java.math.RoundingMode + +internal class SwapMarketsTokenItemConverter( + private val appCurrency: AppCurrency, +) : Converter { + + private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(shouldFormatAxis = false) + + override fun convert(value: TokenMarket): MarketsListItemUM { + return MarketsListItemUM( + id = value.id, + name = value.name, + currencySymbol = value.symbol, + ratingPosition = value.marketRating?.toString(), + marketCap = value.getMarketCap(), + iconUrl = value.imageUrlLarge, + price = value.getCurrentPrice(), + trendPercentText = value.getTrendPercent(), + trendType = value.getTrendType(), + chartData = value.getChartData(), + isUnder100kMarketCap = value.isUnderMarketCapLimit, + stakingRate = value.yieldRate?.format { percent() }?.let { + resourceReference(R.string.markets_apy_placeholder, wrappedList(it)) + }, + updateTimestamp = value.updateTimestamp, + networks = value.networks?.map { network -> + MarketsListItemUM.Network( + networkId = network.networkId, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) + }, + ) + } + + fun convertList(items: List): List = items.map(::convert) + + fun update(prev: TokenMarket, prevUI: MarketsListItemUM, new: TokenMarket): MarketsListItemUM { + require(prev.id == new.id) { + "Ids is not the same during update TokenMarket item: previousItem[${prev.id}] != newItem[${new.id}]" + } + + return prevUI.copy( + name = new.name, + currencySymbol = new.symbol, + ratingPosition = new.marketRating?.toString(), + marketCap = ifChanged(prev.marketCap, new.marketCap, prevUI.marketCap) { new.getMarketCap() }, + iconUrl = new.imageUrlLarge, + price = ifChanged(prev = prev.tokenQuotesShort, new = new.tokenQuotesShort, prevR = prevUI.price) { + new.getCurrentPrice(prev = prev) + }, + trendPercentText = ifChanged( + prev.tokenQuotesShort, + new.tokenQuotesShort, + prevUI.trendPercentText, + ) { new.getTrendPercent() }, + trendType = ifChanged(prev.tokenQuotesShort, new.tokenQuotesShort, prevUI.trendType) { new.getTrendType() }, + chartData = ifChanged(prev.tokenCharts, new.tokenCharts, prevUI.chartData) { new.getChartData() }, + ) + } + + private inline fun ifChanged(prev: T, new: T, prevR: R, force: Boolean = false, change: (T) -> R): R { + return if (force || prev != new) change(new) else prevR + } + + private fun TokenMarket.getMarketCap(): String? { + val value = marketCap?.takeIf { marketCap != BigDecimal.ZERO } ?: return null + + return value.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).compact( + threeDigitsMethod = true, + ) + } + } + + private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price { + val prevPrice = prev?.tokenQuotesShort?.currentPrice + + val priceText = tokenQuotesShort.currentPrice.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ).price() + } + + val changeType = if (prevPrice != null) { + if (tokenQuotesShort.currentPrice > prevPrice) { + PriceChangeType.UP + } else { + PriceChangeType.DOWN + } + } else { + null + } + + return MarketsListItemUM.Price( + text = priceText, + changeType = changeType, + ) + } + + private fun TokenMarket.getChartData(): MarketChartRawData? { + val chart = tokenCharts.h24 + + return chart?.let { ct -> + priceAndTimePointValuesConverter.convert( + MarketChartData.Data( + y = ct.priceY.toImmutableList(), + x = ct.timeStamps.map { it.toBigDecimal() }.toImmutableList(), + ).sorted(), + ) + } + } + + @Suppress("MagicNumber") + private fun TokenMarket.getTrendType(): PriceChangeType { + val percent = tokenQuotesShort.h24ChangePercent + val scaled = percent?.setScale(4, RoundingMode.HALF_UP) + return when { + scaled == null -> PriceChangeType.NEUTRAL + scaled > BigDecimal.ZERO -> PriceChangeType.UP + scaled < BigDecimal.ZERO -> PriceChangeType.DOWN + else -> PriceChangeType.NEUTRAL + } + } + + private fun TokenMarket.getTrendPercent(): String { + val percent = tokenQuotesShort.h24ChangePercent + return percent.format { percent() } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt new file mode 100644 index 0000000000..7e9abb3982 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt @@ -0,0 +1,41 @@ +package com.tangem.features.onramp.swap.availablepairs.market.state + +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.currency.CryptoCurrency +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed class SwapMarketState { + + abstract val marketsTitle: TextReference + abstract val shouldAssetsCount: Boolean + + data class Content( + val items: ImmutableList, + val total: Int, + val loadMore: () -> Unit, + val onItemClick: (MarketsListItemUM) -> Unit, + val visibleIdsChanged: (List) -> Unit, + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, + ) : SwapMarketState() + + data class Loading( + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, + ) : SwapMarketState() + + data class LoadingError( + val onRetryClicked: () -> Unit, + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, + ) : SwapMarketState() + + data object SearchNothingFound : SwapMarketState() { + override val marketsTitle: TextReference = TextReference.Res(R.string.markets_common_title) + override val shouldAssetsCount: Boolean = true + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AddToPortfolioRoute.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AddToPortfolioRoute.kt new file mode 100644 index 0000000000..559ef6eb09 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AddToPortfolioRoute.kt @@ -0,0 +1,7 @@ +package com.tangem.features.onramp.swap.availablepairs.model + +import com.tangem.core.decompose.navigation.Route +import kotlinx.serialization.Serializable + +@Serializable +internal data object AddToPortfolioRoute : Route \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index d862519a45..363f0712d5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -1,53 +1,83 @@ package com.tangem.features.onramp.swap.availablepairs.model +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources +import com.tangem.core.analytics.models.event.SwapAnalyticsEvent import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.fields.InputManager +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenMarketListConfig +import com.tangem.domain.markets.toSerializableParam import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.swap.domain.GetAvailablePairsUseCase import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.feature.swap.domain.models.domain.SwapPairLeast +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetLoadingTokenItemsTransformer import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformer import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformerV2 +import com.tangem.features.onramp.swap.availablepairs.market.SwapMarketsListBatchFlowManager +import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM import com.tangem.features.onramp.swap.entity.AccountCurrencyUM import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMController import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer import com.tangem.features.onramp.tokenlist.entity.transformer.* +import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer +import com.tangem.features.swap.SwapFeatureToggles +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.coroutines.saveIn +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +import com.tangem.core.ui.R as CoreUiR private typealias AvailablePairsState = Lce> @@ -55,6 +85,7 @@ private typealias AvailablePairsState = Lce> internal class AvailableSwapPairsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, private val getTokenListUseCase: GetTokenListUseCase, private val tokenListUMController: TokenListUMController, private val searchManager: InputManager, @@ -64,6 +95,11 @@ internal class AvailableSwapPairsModel @Inject constructor( private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val accountsFeatureToggles: AccountsFeatureToggles, + private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, + private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, + private val excludedBlockchains: ExcludedBlockchains, + private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, + swapFeatureToggles: SwapFeatureToggles, getWalletsUseCase: GetWalletsUseCase, ) : Model() { @@ -71,11 +107,54 @@ internal class AvailableSwapPairsModel @Inject constructor( private val params: AvailableSwapPairsComponent.Params = paramsContainer.require() private val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } + private val allUserWallets = getWalletsUseCase.invokeSync() + + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + var addToPortfolioManager: AddToPortfolioManager? = null + val addToPortfolioCallback: AddToPortfolioComponent.Callback = object : AddToPortfolioComponent.Callback { + override fun onDismiss() = bottomSheetNavigation.dismiss() + override fun onSuccess(addedToken: CryptoCurrency) { + onTokenAddedToPortfolio(addedToken) + } + } + private val addToPortfolioJobHolder = JobHolder() private val tokenListFlow = getTokenListUseCaseFlow() private val accountListFlow = getAccountListUseCaseFlow() private val availablePairsByNetworkFlow = MutableStateFlow>(emptyMap()) + private val selectedAppCurrencyFlow: StateFlow = getSelectedAppCurrencyUseCase.invokeOrDefault() + .stateIn(scope = modelScope, started = SharingStarted.Eagerly, initialValue = AppCurrency.Default) + private val refreshPairsTrigger = MutableSharedFlow() + private val searchQueryStateForMarkets = MutableStateFlow("") + private val visibleMarketItemIds = MutableStateFlow>(emptyList()) + + private val defaultMarketsListManager by lazy { + SwapMarketsListBatchFlowManager( + getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, + order = TokenMarketListConfig.Order.Trending, + currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, + currentSearchText = Provider { null }, + modelScope = modelScope, + dispatchers = dispatchers, + ) + } + + private val searchMarketsListManager by lazy { + SwapMarketsListBatchFlowManager( + getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, + order = TokenMarketListConfig.Order.ByRating, + currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, + currentSearchText = Provider { searchQueryStateForMarkets.value }, + modelScope = modelScope, + dispatchers = dispatchers, + ) + } + + private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) + init { if (accountsFeatureToggles.isFeatureEnabled) { subscribeOnUpdateStateV2() @@ -85,6 +164,11 @@ internal class AvailableSwapPairsModel @Inject constructor( initializeSearchBarCallbacks() subscribeOnAvailablePairsUpdates() + + if (swapFeatureToggles.isMarketListFeatureEnabled) { + subscribeOnMarketsUpdates() + subscribeOnVisibleMarketItems() + } } private fun getTokenListUseCaseFlow(): SharedFlow> { @@ -238,7 +322,7 @@ internal class AvailableSwapPairsModel @Inject constructor( } else { UpdateTokenItemsTransformer( appCurrency = appCurrency, - onItemClick = params.onTokenClick, + onItemClick = ::onPortfolioTokenClick, statuses = filterByQueryTokenList.filterByAvailability(availablePairs = availablePairs), isBalanceHidden = isBalanceHidden, unavailableTokensHeaderReference = resourceReference( @@ -259,7 +343,7 @@ internal class AvailableSwapPairsModel @Inject constructor( ): TokenListUMTransformer { val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding - val filterByQueryAccountList = accountList + val filterByQueryAccountList: Map> = accountList .associate { accountStatus -> when (accountStatus) { is AccountStatus.CryptoPortfolio -> { @@ -272,6 +356,7 @@ internal class AvailableSwapPairsModel @Inject constructor( accountStatus.account to statuses } + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } } .filterValues { it.isNotEmpty() } @@ -296,7 +381,7 @@ internal class AvailableSwapPairsModel @Inject constructor( } else { UpdateAccountTokenListTransformer( appCurrency = appCurrency, - onItemClick = params.onTokenClick, + onItemClick = ::onPortfolioTokenClick, accountList = filterByQueryAccountList.filterByAvailability(availablePairs = availablePairs), isBalanceHidden = isBalanceHidden, unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), @@ -332,7 +417,7 @@ internal class AvailableSwapPairsModel @Inject constructor( onRefresh = { modelScope.launch { if (networkInfo != null) { - accountList.filterIsInstance() + accountList.filterCryptoPortfolio() .forEach { (_, currencies) -> updateAvailablePairs(networkInfo, currencies.flattenCurrencies()) } @@ -344,8 +429,12 @@ internal class AvailableSwapPairsModel @Inject constructor( private fun subscribeOnAvailablePairsUpdates() { modelScope.launch { - params.selectedStatus - .filterNotNull() + combine( + params.selectedStatus.filterNotNull(), + refreshPairsTrigger + .onEach { availablePairsByNetworkFlow.value = emptyMap() } + .onStart { emit(Unit) }, + ) { status, _ -> status } .collectLatest { selectedStatus -> val networkInfo = selectedStatus.toLeastTokenInfo() @@ -356,7 +445,7 @@ internal class AvailableSwapPairsModel @Inject constructor( val accountList = accountListFlow.firstOrNull() ?: return@collectLatest updateAvailablePairs( networkInfo = networkInfo, - statuses = accountList.filterIsInstance() + statuses = accountList.filterCryptoPortfolio() .flatMap { accountStatus -> accountStatus.flattenCurrencies() }.toSet().toList(), @@ -421,6 +510,8 @@ internal class AvailableSwapPairsModel @Inject constructor( tokenListUMController.update(transformer = UpdateSearchQueryTransformer(newQuery)) searchManager.update(newQuery) + + searchQueryStateForMarkets.value = newQuery } } @@ -476,10 +567,214 @@ internal class AvailableSwapPairsModel @Inject constructor( } } + private fun onPortfolioTokenClick(tokenItem: TokenItemState, status: CryptoCurrencyStatus) { + analyticsEventHandler.send( + SwapAnalyticsEvent.TokenSelected( + token = status.currency.symbol, + source = ScreensSources.Portfolio, + isSearched = state.value.searchBarUM.query.isNotEmpty(), + ), + ) + params.onTokenClick(tokenItem, status) + } + private fun CryptoCurrencyStatus.toLeastTokenInfo(): LeastTokenInfo { return LeastTokenInfo( contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", network = currency.network.backendId, ) } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun subscribeOnMarketsUpdates() { + searchQueryStateForMarkets + .map { it.isEmpty() } + .distinctUntilChanged() + .flatMapLatest { isDefaultMode -> + if (isDefaultMode) { + visibleMarketItemIds.value = emptyList() + createDefaultMarketsFlow() + } else { + visibleDefaultMarketItemIds.value = emptyList() + createSearchMarketsFlow() + } + } + .onEach { marketsState -> + tokenListUMController.update { it.copy(marketsState = marketsState) } + } + .flowOn(dispatchers.main) + .launchIn(modelScope) + + searchQueryStateForMarkets + .onEach { searchQuery -> + if (searchQuery.isNotEmpty()) { + searchMarketsListManager.reload(searchQuery) + } + } + .launchIn(modelScope) + + params.selectedStatus + .filterNotNull() + .take(1) + .onEach { defaultMarketsListManager.reload() } + .launchIn(modelScope) + } + + private fun createDefaultMarketsFlow(): Flow { + val marketsTitle = TextReference.Res(CoreUiR.string.feed_trending_now) + return combine( + defaultMarketsListManager.uiItems, + defaultMarketsListManager.isInInitialLoadingErrorState, + defaultMarketsListManager.totalCount, + ) { uiItems, isError, total -> + when { + isError -> SwapMarketState.LoadingError( + onRetryClicked = { defaultMarketsListManager.reload() }, + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + uiItems.isEmpty() -> SwapMarketState.Loading( + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + else -> SwapMarketState.Content( + items = uiItems, + loadMore = { defaultMarketsListManager.loadMore() }, + onItemClick = { item -> addToPortfolioItem(item) }, + visibleIdsChanged = { visibleDefaultMarketItemIds.value = it }, + total = total ?: uiItems.size, + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + } + } + } + + private fun createSearchMarketsFlow(): Flow { + val marketsTitle = TextReference.Res(CoreUiR.string.markets_common_title) + return combine( + flow = searchMarketsListManager.uiItems, + flow2 = searchMarketsListManager.isInInitialLoadingErrorState, + flow3 = searchMarketsListManager.isSearchNotFoundState, + flow4 = searchMarketsListManager.totalCount, + ) { uiItems, isError, isSearchNotFound, total -> + when { + isError -> SwapMarketState.LoadingError( + onRetryClicked = { + searchMarketsListManager.reload(searchQueryStateForMarkets.value) + }, + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) + isSearchNotFound -> SwapMarketState.SearchNothingFound + uiItems.isEmpty() -> SwapMarketState.Loading( + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) + else -> SwapMarketState.Content( + items = uiItems, + loadMore = { searchMarketsListManager.loadMore() }, + onItemClick = { item -> addToPortfolioItem(item) }, + visibleIdsChanged = { visibleMarketItemIds.value = it }, + total = total ?: uiItems.size, + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) + } + } + } + + private fun onTokenAddedToPortfolio(addedToken: CryptoCurrency) { + modelScope.launch { + bottomSheetNavigation.dismiss() + analyticsEventHandler.send( + SwapAnalyticsEvent.TokenSelected( + token = addedToken.symbol, + source = ScreensSources.Markets, + isSearched = state.value.searchBarUM.query.isNotEmpty(), + ), + ) + + // Trigger re-fetch of available pairs (clears cache + re-enters collectLatest) + refreshPairsTrigger.emit(Unit) + + // Wait for the added token status to become Loaded + val addedTokenStatus = getAccountCurrencyStatusUseCase(params.userWalletId, addedToken) + .firstOrNull { it.status.value is CryptoCurrencyStatus.Loaded } + ?.status + ?: return@launch + + // Convert to TokenItemState and trigger token selection → navigates to swap + val converter = OnrampTokenItemStateConverterFactory.createAvailableItemConverter( + appCurrency = selectedAppCurrencyFlow.value, + onItemClick = params.onTokenClick, + ) + params.onTokenClick(converter.convert(addedTokenStatus), addedTokenStatus) + } + } + + private fun addToPortfolioItem(item: MarketsListItemUM) { + modelScope.launch { + val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id) + ?: searchMarketsListManager.getTokenMarketById(item.id) + ?: return@launch + + val param = tokenMarket.toSerializableParam() + val hasOnlyHotWallets = allUserWallets.all { it is UserWallet.Hot } + + val networks = tokenMarket.networks?.filter { network -> + BlockchainUtils.isSupportedNetworkId( + blockchainId = network.networkId, + excludedBlockchains = excludedBlockchains, + hotExcludedBlockchains = hotWalletExcludedBlockchains, + hasOnlyHotWallets = hasOnlyHotWallets, + ) + }?.map { network -> + TokenMarketInfo.Network( + networkId = network.networkId, + isExchangeable = false, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) + }.orEmpty() + + addToPortfolioManager = addToPortfolioManagerFactory + .create( + scope = modelScope, + token = param, + analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value), + ).apply { + setTokenNetworks(networks) + } + + addToPortfolioManager?.state + ?.firstOrNull { it is AddToPortfolioManager.State.AvailableToAdd } + ?.run { bottomSheetNavigation.activate(AddToPortfolioRoute) } + }.saveIn(addToPortfolioJobHolder) + } + + private fun subscribeOnVisibleMarketItems() { + modelScope.launch { + visibleMarketItemIds.mapNotNull { rawIds -> + if (rawIds.isNotEmpty()) { + searchMarketsListManager.getBatchKeysByItemIds(rawIds) + } else { + null + } + }.distinctUntilChanged().collectLatest { visibleBatchKeys -> + searchMarketsListManager.loadCharts(visibleBatchKeys) + } + } + modelScope.launch { + visibleDefaultMarketItemIds.mapNotNull { rawIds -> + if (rawIds.isNotEmpty()) { + defaultMarketsListManager.getBatchKeysByItemIds(rawIds) + } else { + null + } + }.distinctUntilChanged().collectLatest { visibleBatchKeys -> + defaultMarketsListManager.loadCharts(visibleBatchKeys) + } + } + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt new file mode 100644 index 0000000000..18600da888 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt @@ -0,0 +1,114 @@ +package com.tangem.features.onramp.swap.availablepairs.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import com.tangem.common.ui.markets.MarketsListItem +import com.tangem.common.ui.markets.MarketsListItemPlaceholder +import com.tangem.core.ui.R +import com.tangem.core.ui.components.UnableToLoadData +import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState + +private const val LOADING_PLACEHOLDERS_COUNT = 20 + +internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) { + item(key = "markets_title") { + val totalCount = (state as? SwapMarketState.Content)?.total + Text( + text = buildAnnotatedString { + append(state.marketsTitle.resolveReference()) + if (totalCount != null) { + withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { + append(" $totalCount") + } + } + }, + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16) + .padding(top = TangemTheme.dimens.spacing24, bottom = TangemTheme.dimens.spacing12), + ) + } + + when (state) { + is SwapMarketState.Loading -> { + items(count = LOADING_PLACEHOLDERS_COUNT, key = { "market_placeholder_$it" }) { + MarketsListItemPlaceholder() + } + } + is SwapMarketState.LoadingError -> { + item(key = "market_loading_error") { + LoadingErrorItem( + modifier = Modifier.fillParentMaxWidth(), + onTryAgain = state.onRetryClicked, + ) + } + } + SwapMarketState.SearchNothingFound -> { + item(key = "market_not_found") { + SearchNothingFoundText( + modifier = Modifier.fillParentMaxWidth(), + ) + } + } + is SwapMarketState.Content -> { + itemsIndexed( + items = state.items, + key = { _, item -> item.getComposeKey() }, + ) { index, item -> + MarketsListItem( + model = item, + onClick = { state.onItemClick(item) }, + modifier = Modifier.roundedShapeItemDecoration( + currentIndex = index, + lastIndex = state.items.lastIndex, + backgroundColor = TangemTheme.colors.background.action, + ), + ) + } + } + } +} + +@Composable +private fun LoadingErrorItem(onTryAgain: () -> Unit, modifier: Modifier = Modifier) { + Box( + modifier + .padding( + horizontal = TangemTheme.dimens.spacing16, + vertical = TangemTheme.dimens.spacing12, + ), + contentAlignment = Alignment.Center, + ) { + UnableToLoadData(onRetryClick = onTryAgain) + } +} + +@Composable +private fun SearchNothingFoundText(modifier: Modifier = Modifier) { + Box( + modifier = modifier.padding(TangemTheme.dimens.spacing16), + contentAlignment = Alignment.Center, + ) { + Text( + text = stringResourceSafe(R.string.markets_search_token_no_result_title), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.tertiary, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/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/swap/ui/SwapSelectTokens.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt index 346d2b498f..5d3e457cc8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt @@ -8,24 +8,33 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent +import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState import com.tangem.features.onramp.swap.entity.ExchangeCardUM import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent import com.tangem.features.onramp.tokenlist.entity.TokenListUM +private const val LOAD_MORE_BUFFER = 25 + /** * Swap select tokens * @@ -49,8 +58,8 @@ internal fun SwapSelectTokens( BackHandler(onBack = state.onBackClick) val nestedScrollConnection = rememberHideKeyboardNestedScrollConnection() - val lazyListState = rememberLazyListState() + LazyColumn( modifier = modifier .nestedScroll(nestedScrollConnection) @@ -60,62 +69,146 @@ internal fun SwapSelectTokens( state = lazyListState, contentPadding = PaddingValues(bottom = 8.dp), ) { - stickyHeader(key = "header") { - AppBarWithBackButton( - onBackClick = state.onBackClick, - text = stringResourceSafe(id = R.string.common_swap), - iconRes = R.drawable.ic_close_24, - containerColor = TangemTheme.colors.background.secondary, - ) - } + swapSelectTokensContent( + state = state, + selectFromTokenListComponent = selectFromTokenListComponent, + selectFromTokenListState = selectFromTokenListState, + selectToTokenListComponent = selectToTokenListComponent, + selectToTokenListState = selectToTokenListState, + ) + } - item(key = "exchange_from", contentType = "exchange_from") { + ScrollToTopEffect(state = state, lazyListState = lazyListState) + + MarketsHandlers( + state = state, + selectToTokenListState = selectToTokenListState, + lazyListState = lazyListState, + ) +} + +@OptIn(ExperimentalFoundationApi::class) +private fun LazyListScope.swapSelectTokensContent( + state: SwapSelectTokensUM, + selectFromTokenListComponent: OnrampTokenListComponent, + selectFromTokenListState: TokenListUM, + selectToTokenListComponent: AvailableSwapPairsComponent, + selectToTokenListState: TokenListUM, +) { + stickyHeader(key = "header") { + AppBarWithBackButton( + onBackClick = state.onBackClick, + text = stringResourceSafe(id = R.string.common_swap), + iconRes = R.drawable.ic_close_24, + containerColor = TangemTheme.colors.background.secondary, + ) + } + + item(key = "exchange_from", contentType = "exchange_from") { + ExchangeCard( + state = state.exchangeFrom, + isBalanceHidden = state.isBalanceHidden, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 8.dp, bottom = 12.dp) + .animateItem(), + ) + } + + if (state.exchangeFrom is ExchangeCardUM.Empty) { + with(selectFromTokenListComponent) { + content(uiState = selectFromTokenListState, modifier = Modifier) + } + } + + if (state.exchangeFrom is ExchangeCardUM.Filled) { + exchangeToSection( + state = state, + selectToTokenListComponent = selectToTokenListComponent, + selectToTokenListState = selectToTokenListState, + ) + } +} + +@OptIn(ExperimentalFoundationApi::class) +private fun LazyListScope.exchangeToSection( + state: SwapSelectTokensUM, + selectToTokenListComponent: AvailableSwapPairsComponent, + selectToTokenListState: TokenListUM, +) { + item(key = "exchange_to", contentType = "exchange_to") { + if (selectToTokenListState.warning != NotificationUM.Warning.SwapNoAvailablePair) { ExchangeCard( - state = state.exchangeFrom, + state = state.exchangeTo, isBalanceHidden = state.isBalanceHidden, modifier = Modifier .padding(horizontal = 16.dp) - .padding(top = 8.dp, bottom = 12.dp) + .padding(bottom = 12.dp) .animateItem(), ) } - - if (state.exchangeFrom is ExchangeCardUM.Empty) { - with(selectFromTokenListComponent) { - content( - uiState = selectFromTokenListState, - modifier = Modifier, - ) - } - } - - if (state.exchangeFrom is ExchangeCardUM.Filled) { - item(key = "exchange_to", contentType = "exchange_to") { - if (selectToTokenListState.warning != NotificationUM.Warning.SwapNoAvailablePair) { - ExchangeCard( - state = state.exchangeTo, - isBalanceHidden = state.isBalanceHidden, - modifier = Modifier - .padding(horizontal = 16.dp) - .padding(bottom = 12.dp) - .animateItem(), - ) - } - } - - if (state.exchangeTo is ExchangeCardUM.Empty) { - with(selectToTokenListComponent) { - content( - uiState = selectToTokenListState, - modifier = Modifier.padding(horizontal = 16.dp), - ) - } - } - } } - // scroll to top after "from" token selection + if (state.exchangeTo is ExchangeCardUM.Empty) { + with(selectToTokenListComponent) { + content(uiState = selectToTokenListState, modifier = Modifier.padding(horizontal = 16.dp)) + } + } +} + +@Composable +private fun ScrollToTopEffect(state: SwapSelectTokensUM, lazyListState: LazyListState) { LaunchedEffect(state.exchangeFrom !is ExchangeCardUM.Empty) { lazyListState.scrollToItem(index = 0) } +} + +@Composable +private fun MarketsHandlers( + state: SwapSelectTokensUM, + selectToTokenListState: TokenListUM, + lazyListState: LazyListState, +) { + // Markets for "to" token list + if (state.exchangeFrom is ExchangeCardUM.Filled && state.exchangeTo is ExchangeCardUM.Empty) { + MarketsPaginationHandler( + marketsState = selectToTokenListState.marketsState, + lazyListState = lazyListState, + ) + } +} + +@Composable +private fun MarketsPaginationHandler(marketsState: SwapMarketState?, lazyListState: LazyListState) { + (marketsState as? SwapMarketState.Content)?.let { content -> + VisibleItemsTracker(lazyListState = lazyListState, marketState = content) + + InfiniteListHandler( + listState = lazyListState, + buffer = LOAD_MORE_BUFFER, + triggerLoadMoreCheckOnItemsCountChange = true, + onLoadMore = remember(content) { + { + content.loadMore() + true + } + }, + ) + } +} + +@Composable +private fun VisibleItemsTracker(lazyListState: LazyListState, marketState: SwapMarketState.Content) { + val visibleItems by remember { + derivedStateOf { + lazyListState.layoutInfo.visibleItemsInfo + .mapNotNull { itemInfo -> + marketState.items.find { it.getComposeKey() == itemInfo.key }?.id + } + } + } + + LaunchedEffect(visibleItems) { + marketState.visibleIdsChanged(visibleItems) + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt index 3f88743a05..04f7008ff8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt @@ -3,6 +3,7 @@ package com.tangem.features.onramp.tokenlist.entity import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState import kotlinx.collections.immutable.ImmutableList /** @@ -12,6 +13,7 @@ import kotlinx.collections.immutable.ImmutableList * @property availableItems available items (search bar, header, tokens) * @property unavailableItems unavailable items (header, tokens) * @property isBalanceHidden flag that indicates if balance should be hidden + * @property marketsState markets list state (null when markets should not be shown) * [REDACTED_AUTHOR] */ @@ -22,16 +24,24 @@ internal data class TokenListUM( val tokensListData: TokenListUMData, val isBalanceHidden: Boolean, val warning: NotificationUM? = null, + val marketsState: SwapMarketState? = null, ) internal sealed interface TokenListUMData { + + val totalTokensCount: Int + data class AccountList( val tokensList: ImmutableList, + override val totalTokensCount: Int, ) : TokenListUMData data class TokenList( val tokensList: ImmutableList, + override val totalTokensCount: Int, ) : TokenListUMData - data object EmptyList : TokenListUMData + data object EmptyList : TokenListUMData { + override val totalTokensCount: Int = 0 + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt index 48d7b7331d..a7ca52c790 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt @@ -2,6 +2,8 @@ package com.tangem.features.onramp.tokenlist.entity.transformer import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingAccountTokenItemConverter import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingTokenListItemConverter import com.tangem.features.onramp.tokenlist.entity.TokenListUM @@ -19,24 +21,34 @@ internal class SetLoadingAccountTokenListTransformer( private val accountListItemConverter = LoadingAccountTokenItemConverter(appCurrency) override fun transform(prevState: TokenListUM): TokenListUM { + val totalTokensCount = accountList.sumOf { account -> + when (account) { + is AccountStatus.CryptoPortfolio -> account.tokenList.flattenCurrencies().size + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") + } + } + return prevState.copy( availableItems = persistentListOf(), unavailableItems = persistentListOf(), tokensListData = if (isAccountsMode) { TokenListUMData.AccountList( tokensList = accountListItemConverter.convertList( - accountList.filterIsInstance(), + accountList.filterCryptoPortfolio(), ).toPersistentList(), + totalTokensCount = totalTokensCount, ) } else { TokenListUMData.TokenList( tokensList = accountList.flatMap { account -> when (account) { is AccountStatus.CryptoPortfolio -> LoadingTokenListItemConverter.convertList( - account.tokenList.flattenCurrencies(), + account.tokenList.flattenCurrencies().map(CryptoCurrencyStatus::currency), ) + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } }.toPersistentList(), + totalTokensCount = totalTokensCount, ) }, warning = null, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformerV2.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformerV2.kt index ddc0a2ac1f..422e445dff 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformerV2.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformerV2.kt @@ -23,6 +23,7 @@ internal class SetNothingToFoundStateTransformerV2( text = emptySearchMessageReference, ), ), + totalTokensCount = 0, ), isBalanceHidden = isBalanceHidden, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt index 51fe76f5af..8ccc9dabe0 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt @@ -40,12 +40,15 @@ internal class UpdateAccountTokenListTransformer( .createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText) override fun transform(prevState: TokenListUM): TokenListUM { + val totalTokensCount = accountList.sumOf { it.currencyList.size } + return prevState.copy( availableItems = persistentListOf(), unavailableItems = persistentListOf(), tokensListData = if (isAccountsMode) { TokenListUMData.AccountList( tokensList = accountListItemConverter.convertList(accountList).toPersistentList(), + totalTokensCount = totalTokensCount, ) } else { val tokensList = accountList.flatMap { (_, currencyList) -> @@ -66,6 +69,7 @@ internal class UpdateAccountTokenListTransformer( text = resourceReference(R.string.exchange_tokens_available_tokens_header), ), ) + tokensList, + totalTokensCount = totalTokensCount, ) } else { TokenListUMData.EmptyList diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/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/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index e36eabad90..a90f9e6e25 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -295,13 +295,16 @@ internal class OnrampTokenListModel @Inject constructor( ) } - private fun AccountStatusList.filterAccountsByQuery(query: String) = accountStatuses.asSequence() + private fun AccountStatusList.filterAccountsByQuery( + query: String, + ): Map> = accountStatuses.asSequence() .associate { accountStatus -> when (accountStatus) { is AccountStatus.CryptoPortfolio -> { val filteredList = accountStatus.tokenList.flattenCurrencies().filterByQuery(query = query) accountStatus.account to filteredList } + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } }.filter { (_, value) -> value.isNotEmpty() } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index aefd164597..e878df7dbd 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -3,18 +3,25 @@ package com.tangem.features.onramp.tokenlist.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH32 import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.TangemSearchBarDefaults import com.tangem.core.ui.components.fields.entity.SearchBarUM @@ -25,25 +32,82 @@ import com.tangem.core.ui.components.tokenlist.TokenListItem import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.extensions.conditional +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.BuyTokenScreenTestTags import com.tangem.core.ui.utils.lazyListItemPosition +import com.tangem.features.onramp.swap.availablepairs.ui.swapMarketsListItems import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMData import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider import kotlinx.collections.immutable.ImmutableList /** - * Token list + * Token list for swap - automatically switches between normal and search mode with markets * - * @param state state + * @param state state * -[REDACTED_AUTHOR] + */ +internal fun LazyListScope.onrampSwapTokenList(state: TokenListUM) { + if (state.marketsState != null) { + onrampTokenListWithMarkets(state = state) + } else { + onrampTokenList(state = state) + } +} + +/** + * Token list - normal mode (without markets) + * + * @param state state */ internal fun LazyListScope.onrampTokenList(state: TokenListUM) { val itemModifier = Modifier.padding(horizontal = 16.dp) + warningOrSearchBar(state = state, itemModifier = itemModifier) + + tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) + + tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden) + + tokensListData(state = state) +} + +/** + * Token list with markets - search mode + * + * @param state state + */ +private fun LazyListScope.onrampTokenListWithMarkets(state: TokenListUM) { + val itemModifier = Modifier.padding(horizontal = 16.dp) + + warningOrSearchBar(state = state, itemModifier = itemModifier) + + // Check if user has any assets to show + val hasAssets = state.availableItems.isNotEmpty() || + state.unavailableItems.isNotEmpty() || + state.tokensListData.totalTokensCount != 0 + + if (hasAssets) { + assetsTitle( + count = state.tokensListData.totalTokensCount, + showCount = state.marketsState?.shouldAssetsCount == true, + ) + + tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) + + tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden) + + tokensListData(state = state) + + item { SpacerH32() } + } + + state.marketsState?.let(::swapMarketsListItems) +} + +private fun LazyListScope.warningOrSearchBar(state: TokenListUM, itemModifier: Modifier) { if (state.warning == null) { searchBarItem(searchBarUM = state.searchBarUM, modifier = itemModifier) } else { @@ -67,11 +131,9 @@ internal fun LazyListScope.onrampTokenList(state: TokenListUM) { } } } +} - tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) - - tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden) - +private fun LazyListScope.tokensListData(state: TokenListUM) { when (val list = state.tokensListData) { is TokenListUMData.AccountList -> list.tokensList.forEach { item -> portfolioTokensList( @@ -99,6 +161,30 @@ private fun LazyListScope.searchBarItem(searchBarUM: SearchBarUM, modifier: Modi } } +private fun LazyListScope.assetsTitle(count: Int, showCount: Boolean) { + item(key = "assets_title") { + Text( + text = buildAnnotatedString { + append(stringResourceSafe(R.string.swap_your_assets_title)) + if (showCount) { + withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { + append(" $count") + } + } + }, + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + modifier = Modifier + .fillMaxWidth() + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing12, + ), + ) + } +} + private fun LazyListScope.tokensList(items: ImmutableList, isBalanceHidden: Boolean) { itemsIndexed( items = items, @@ -127,7 +213,7 @@ internal fun LazyListScope.portfolioTokensList(portfolio: TokensListItemUM.Portf portfolioItem( portfolio = portfolio, - modifier = Modifier.padding(top = 8.dp), + modifier = Modifier, isBalanceHidden = isBalanceHidden, ) if (!isExpanded) return diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt index 9bc5ac4c5d..5e766048e2 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/ReferralRepositoryImpl.kt @@ -8,7 +8,8 @@ import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.StartReferralBody -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId @@ -27,8 +28,8 @@ typealias ExternalReferralRepository = com.tangem.domain.referral.ReferralReposi internal class ReferralRepositoryImpl @Inject constructor( private val referralApi: TangemTechApi, private val referralConverter: ReferralConverter, - private val coroutineDispatcher: CoroutineDispatcherProvider, - private val userWalletsStore: UserWalletsStore, + private val userWalletsListRepository: UserWalletsListRepository, + private val dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, ) : ReferralRepository, ExternalReferralRepository { @@ -38,7 +39,7 @@ internal class ReferralRepositoryImpl @Inject constructor( private val referralStatus: ConcurrentHashMap = ConcurrentHashMap() override suspend fun getReferralData(walletId: String): ReferralData { - return withContext(coroutineDispatcher.io) { + return withContext(dispatchers.io) { val referralData = referralConverter.convert( referralApi.getReferralStatus( walletId = walletId, @@ -76,7 +77,7 @@ internal class ReferralRepositoryImpl @Inject constructor( tokenId: String, address: String, ): ReferralData { - return withContext(coroutineDispatcher.io) { + return withContext(dispatchers.io) { val referralData = referralConverter.convert( referralApi.startReferral( startReferralBody = StartReferralBody( @@ -97,9 +98,7 @@ internal class ReferralRepositoryImpl @Inject constructor( tokenData: TokenData, accountIndex: DerivationIndex?, ): CryptoCurrency? { - val userWallet = withContext(coroutineDispatcher.io) { - userWalletsStore.getSyncOrNull(userWalletId) ?: error("Wallet $userWalletId not found") - } + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) val blockchain = Blockchain.fromNetworkId(tokenData.networkId) ?: error("Blockchain ${tokenData.networkId} not found") diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt index 2170e1651d..af0876c251 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt @@ -3,7 +3,7 @@ package com.tangem.feature.referral.di import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.feature.referral.converters.ReferralConverter import com.tangem.feature.referral.data.DefaultMobileWalletPromoRepository import com.tangem.feature.referral.data.ExternalReferralRepository @@ -26,15 +26,15 @@ class ReferralRepositoryModule { fun provideReferralRepository( tangemTechApi: TangemTechApi, referralConverter: ReferralConverter, - coroutineDispatcherProvider: CoroutineDispatcherProvider, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, + dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, ): ReferralRepository { return ReferralRepositoryImpl( referralApi = tangemTechApi, referralConverter = referralConverter, - coroutineDispatcher = coroutineDispatcherProvider, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, + dispatchers = dispatchers, excludedBlockchains = excludedBlockchains, ) } @@ -44,15 +44,15 @@ class ReferralRepositoryModule { fun provideExternalReferralRepository( tangemTechApi: TangemTechApi, referralConverter: ReferralConverter, - coroutineDispatcherProvider: CoroutineDispatcherProvider, - userWalletsStore: UserWalletsStore, + userWalletsListRepository: UserWalletsListRepository, + dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, ): ExternalReferralRepository { return ReferralRepositoryImpl( referralApi = tangemTechApi, referralConverter = referralConverter, - coroutineDispatcher = coroutineDispatcherProvider, - userWalletsStore = userWalletsStore, + userWalletsListRepository = userWalletsListRepository, + dispatchers = dispatchers, excludedBlockchains = excludedBlockchains, ) } diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index c7f8ef28fe..930418c920 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -6,6 +6,7 @@ import com.tangem.domain.account.producer.SingleAccountProducer import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.account.supplier.SingleAccountSupplier import com.tangem.domain.models.PortfolioId +import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId @@ -55,7 +56,10 @@ internal class ReferralInteractorImpl( ) ?: error("Account not found: ${portfolioId.accountId}") - account.derivationIndex + when (account) { + is Account.CryptoPortfolio -> account.derivationIndex + is Account.Payment -> TODO("[REDACTED_JIRA]") + } } is PortfolioId.Wallet -> null } @@ -69,7 +73,17 @@ internal class ReferralInteractorImpl( when (portfolioId) { is PortfolioId.Account -> { - manageCryptoCurrenciesUseCase(accountId = portfolioId.accountId, add = cryptoCurrency) + manageCryptoCurrenciesUseCase( + accountId = portfolioId.accountId, + add = cryptoCurrency, + skipDerivationErrors = false, + ).mapLeft { + it.mapToDomainError() + }.onLeft { error -> + if (error is ReferralError.UserCancelledException) { + throw error + } + } } is PortfolioId.Wallet -> { derivePublicKeysUseCase(userWallet.walletId, listOf(cryptoCurrency)).getOrElse { throwable -> 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/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt index 46ce302791..eddb1e949d 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt @@ -127,6 +127,7 @@ internal class ReferralModel @Inject constructor( val cryptoPortfolio = when (selectedAccount) { is AccountStatus.CryptoPortfolio -> selectedAccount + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } val awardCryptoCurrency = referralInteractor.getCryptoCurrency( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt index 78b805b69a..d066f0da3a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt @@ -22,10 +22,13 @@ internal class SendConfirmAlertFactory @Inject constructor( title = resourceReference(id = R.string.send_alert_transaction_failed_title), message = resourceReference(id = R.string.common_unknown_error), onDismissRequest = popBack, - firstAction = EventMessageAction( - title = resourceReference(R.string.common_support), - onClick = onFailedTxEmailClick, - ), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_support), + onClick = onFailedTxEmailClick, + ) + }, + secondActionBuilder = { cancelAction { } }, ), ) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt index 2230676a18..83fb037866 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/DefaultSendComponent.kt @@ -21,6 +21,7 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.SendComponent diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt index eab26f440b..abf8b6eaa0 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt @@ -3,6 +3,7 @@ package com.tangem.features.send.v2.send.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FROM +import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_TO import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.ENS_ADDRESS import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TOKEN @@ -29,14 +30,16 @@ internal sealed class SendAnalyticEvents( val isNonceNotEmpty: Boolean, private val ensStatus: AnalyticsParam.EmptyFull, private val feeToken: String, - val derivationIndex: Int?, + private val fromDerivationIndex: Int?, + private val toDerivationIndex: Int?, ) : SendAnalyticEvents( event = "Transaction Sent Screen Opened", params = buildMap { put(TOKEN_PARAM, token) put(FEE_TYPE, feeType.value) put(BLOCKCHAIN, blockchain) - if (derivationIndex != null) put(ACCOUNT_DERIVATION_FROM, derivationIndex.toString()) + if (fromDerivationIndex != null) put(ACCOUNT_DERIVATION_FROM, fromDerivationIndex.toString()) + if (toDerivationIndex != null) put(ACCOUNT_DERIVATION_TO, toDerivationIndex.toString()) put(NONCE, isNonceNotEmpty.toString().capitalize()) val ensAddress = when (ensStatus) { AnalyticsParam.EmptyFull.Empty -> false.toString().capitalize() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt index 5626f5a93c..c3d536b383 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt @@ -4,7 +4,9 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped +import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCase import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.send.v2.api.entity.FeeNonce import com.tangem.features.send.v2.api.entity.FeeSelectorUM @@ -16,20 +18,24 @@ import javax.inject.Inject @ModelScoped internal class SendAnalyticHelper @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, + private val getAccountCurrencyByAddressUseCase: GetAccountCurrencyByAddressUseCase, ) { - fun sendSuccessAnalytics( + suspend fun sendSuccessAnalytics( cryptoCurrency: CryptoCurrency, sendUM: SendUM, feeToken: CryptoCurrency, - account: Account.CryptoPortfolio?, + account: Account?, ) { val destinationUM = sendUM.destinationUM as? DestinationUM.Content val feeSelectorUM = sendUM.feeSelectorUM as? FeeSelectorUM.Content ?: return val feeType = feeSelectorUM.toAnalyticType() val feeTokenSymbol = feeToken.symbol - val isNotMainAccount = account != null && !account.isMainAccount - val derivationIndex = if (isNotMainAccount) account.derivationIndex.value else null + val fromDerivationIndex = account?.derivationIndex?.value + val destination = destinationUM?.addressTextField?.actualAddress ?: return + val destinationAccount = getAccountCurrencyByAddressUseCase(destination) + .getOrNull()?.account + val toDerivationIndex = destinationAccount?.derivationIndex?.value analyticsEventHandler.send( SendAnalyticEvents.TransactionScreenOpened( token = cryptoCurrency.symbol, @@ -37,7 +43,8 @@ internal class SendAnalyticHelper @Inject constructor( blockchain = cryptoCurrency.network.name, isNonceNotEmpty = feeSelectorUM.feeNonce is FeeNonce.Nonce, ensStatus = getEnsStatus(sendUM), - derivationIndex = derivationIndex, + fromDerivationIndex = fromDerivationIndex, + toDerivationIndex = toDerivationIndex, feeToken = feeTokenSymbol, ), ) @@ -49,7 +56,7 @@ internal class SendAnalyticHelper @Inject constructor( feeType = feeType, feeToken = feeTokenSymbol, ), - memoType = getSendTransactionMemoType(destinationUM?.memoTextField), + memoType = getSendTransactionMemoType(destinationUM.memoTextField), ), ) } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt index 423fc07b00..7bdbc5bb26 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/SendConfirmComponent.kt @@ -158,7 +158,7 @@ internal class SendConfirmComponent( val feeCryptoCurrencyStatus: CryptoCurrencyStatus, val cryptoCurrencyStatusFlow: StateFlow, val feeCryptoCurrencyStatusFlow: StateFlow, - val accountFlow: StateFlow, + val accountFlow: StateFlow, val isAccountModeFlow: StateFlow, val appCurrency: AppCurrency, val callback: ModelCallback, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index a18cc27acf..3411103f5c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -23,8 +23,6 @@ import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -35,6 +33,7 @@ import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.settings.NeverShowTapHelpUseCase import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase @@ -435,12 +434,14 @@ internal class SendConfirmModel @Inject constructor( updateTransactionStatus(txData, txHash) addTokenToWalletIfNeeded() sendBalanceUpdater.scheduleUpdates() - sendAnalyticHelper.sendSuccessAnalytics( - cryptoCurrency = cryptoCurrency, - sendUM = uiState.value, - account = params.accountFlow.value, - feeToken = feeToken, - ) + modelScope.launch(dispatchers.default) { + sendAnalyticHelper.sendSuccessAnalytics( + cryptoCurrency = cryptoCurrency, + sendUM = uiState.value, + account = params.accountFlow.value, + feeToken = feeToken, + ) + } params.callback.onResult(uiState.value) params.onSendTransaction() }, @@ -624,10 +625,7 @@ internal class SendConfirmModel @Inject constructor( private fun getPrimaryButtonText(confirmUM: ConfirmUM, isHoldToConfirm: Boolean): TextReference { return when { - isHoldToConfirm -> resourceReference( - id = com.tangem.core.ui.R.string.common_hold_to, - formatArgs = wrappedList(resourceReference(R.string.common_send)), - ) + isHoldToConfirm -> resourceReference(R.string.common_send) confirmUM is ConfirmUM.Success -> resourceReference(R.string.common_close) confirmUM is ConfirmUM.Content && confirmUM.isSending -> resourceReference(R.string.send_sending) else -> resourceReference(R.string.common_send) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index 2f0b7ffd83..c8eeee19df 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -147,7 +147,7 @@ internal class SendModel @Inject constructor( ), ) - val accountFlow: StateFlow + val accountFlow: StateFlow field = MutableStateFlow(null) val isAccountModeFlow: StateFlow field = MutableStateFlow(false) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt index d776414db6..73eac2a41a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponentParams.kt @@ -24,7 +24,7 @@ internal sealed class SendAmountComponentParams { abstract val cryptoCurrency: CryptoCurrency abstract val cryptoCurrencyStatusFlow: StateFlow abstract val isBalanceHidingFlow: StateFlow - abstract val accountFlow: StateFlow + abstract val accountFlow: StateFlow abstract val isAccountModeFlow: StateFlow data class AmountParams( @@ -37,7 +37,7 @@ internal sealed class SendAmountComponentParams { override val cryptoCurrencyStatusFlow: StateFlow, override val isBalanceHidingFlow: StateFlow, override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource, - override val accountFlow: StateFlow, + override val accountFlow: StateFlow, override val isAccountModeFlow: StateFlow, val callback: ModelCallback, val currentRoute: StateFlow, @@ -53,7 +53,7 @@ internal sealed class SendAmountComponentParams { override val cryptoCurrencyStatusFlow: StateFlow, override val isBalanceHidingFlow: StateFlow, override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource, - override val accountFlow: StateFlow, + override val accountFlow: StateFlow, override val isAccountModeFlow: StateFlow, val userWallet: UserWallet, val blockClickEnableFlow: StateFlow, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt index b232d081f9..21fdc47353 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/model/SendAmountModel.kt @@ -149,7 +149,7 @@ internal class SendAmountModel @Inject constructor( private suspend fun initMinBoundary( cryptoCurrencyStatus: CryptoCurrencyStatus, - account: Account.CryptoPortfolio?, + account: Account?, isAccountsMode: Boolean, ) { minAmountBoundary = getMinimumTransactionAmountSyncUseCase( @@ -178,11 +178,7 @@ internal class SendAmountModel @Inject constructor( } } - private fun initialState( - cryptoCurrencyStatus: CryptoCurrencyStatus, - account: Account.CryptoPortfolio?, - isAccountsMode: Boolean, - ) { + private fun initialState(cryptoCurrencyStatus: CryptoCurrencyStatus, account: Account?, isAccountsMode: Boolean) { val userWallet = userWallet if (uiState.value is AmountState.Empty && userWallet != null) { val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1 @@ -411,10 +407,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/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index ba2ab8f1b1..96690cb647 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -14,7 +14,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase -import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.network.CryptoCurrencyAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isLocked @@ -272,36 +272,40 @@ internal class SendDestinationModel @Inject constructor( return combine( flow = getWalletsUseCase().conflate(), flow2 = multiAccountStatusListSupplier().conflate(), - ) { wallets, accountList -> + ) { wallets, accountStatusLists -> val cryptoCurrencyNetwork = cryptoCurrency.network coroutineScope { - accountList.mapNotNull { accountStatusList -> - val wallet = - wallets.filterNot { it.isLocked }.firstOrNull { it.walletId == accountStatusList.userWalletId } - ?: return@mapNotNull null + accountStatusLists.mapNotNull { accountStatusList -> + val wallet = wallets + .filterNot { it.isLocked } + .firstOrNull { it.walletId == accountStatusList.userWalletId } + ?: return@mapNotNull null async { - accountStatusList.accountStatuses.map { accountStatus -> - async { - accountStatus.flattenCurrencies() - .filter { it.currency.network.rawId == cryptoCurrencyNetwork.rawId } - .mapNotNull { cryptoCurrencyStatus -> - val address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value - ?: return@mapNotNull null + accountStatusList.flattenCurrencies() + .filter { it.currency.network.rawId == cryptoCurrencyNetwork.rawId } + .mapNotNull { cryptoCurrencyStatus -> + val address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: return@mapNotNull null - async { - DestinationWalletUM( - name = wallet.name, - address = address, - cryptoCurrency = cryptoCurrencyStatus.currency, - userWalletId = wallet.walletId, - account = accountStatus.account as? Account.CryptoPortfolio, - ) - } - }.awaitAll() + // Find the corresponding account from accountStatuses + val account = accountStatusList.accountStatuses + .filterCryptoPortfolio() + .firstOrNull { accountStatus -> + accountStatus.tokenList + .flattenCurrencies() + .any { it.currency.id == cryptoCurrencyStatus.currency.id } + }?.account + + DestinationWalletUM( + name = wallet.name, + address = address, + cryptoCurrency = cryptoCurrencyStatus.currency, + userWalletId = wallet.walletId, + account = account, + ) } - }.awaitAll().flatten() } }.awaitAll().flatten() } @@ -319,8 +323,9 @@ internal class SendDestinationModel @Inject constructor( senderAddresses = senderAddresses.value, ) val memoValidationResult = validateWalletMemoUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, memo = memo.orEmpty(), - network = cryptoCurrency.network, ) if (type != null) { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt index c70c805b16..5b47c7de51 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/converters/SendRecipientWalletListConverter.kt @@ -1,9 +1,11 @@ package com.tangem.features.send.v2.subcomponents.destination.model.converters +import com.tangem.common.ui.account.AccountIconUM import com.tangem.common.ui.account.AccountTitleUM import com.tangem.common.ui.account.CryptoPortfolioIconConverter import com.tangem.common.ui.account.toUM import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationRecipientListUM import com.tangem.features.send.v2.subcomponents.destination.model.transformers.WALLET_DEFAULT_COUNT @@ -59,7 +61,12 @@ internal class SendRecipientWalletListConverter( accountTitleUM = if (account != null && isAccountsMode) { AccountTitleUM.Account( name = account.accountName.toUM().value, - icon = CryptoPortfolioIconConverter.convert(account.icon), + icon = when (account) { + is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert( + account.icon, + ) + is Account.Payment -> AccountIconUM.Payment + }, prefixText = stringReference(StringsSigns.DOT), ) } else { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationWalletUM.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationWalletUM.kt index f9f611393b..7cdbd207a8 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationWalletUM.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/ui/state/DestinationWalletUM.kt @@ -20,5 +20,5 @@ data class DestinationWalletUM( val userWalletId: UserWalletId, val address: String, val cryptoCurrency: CryptoCurrency, - val account: Account.CryptoPortfolio? = null, + val account: Account? = null, ) \ No newline at end of file diff --git a/features/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..00409eed5c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -18,11 +18,15 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles +import com.tangem.domain.models.wallet.isColdWallet +import com.tangem.domain.models.wallet.isHotWallet import javax.inject.Inject @ModelScoped internal class StakingStateController @Inject constructor( urlOpener: UrlOpener, + private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles, ) { val value: StakingUiState get() = uiState.value @@ -35,9 +39,11 @@ 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 = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled && + userWallet.isHotWallet, ) } } @@ -98,6 +104,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..00b3379362 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -45,7 +45,8 @@ 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(), @@ -54,6 +55,7 @@ internal class SetButtonsStateTransformer( isIconVisible = isIconVisible, shouldShowProgress = isInProgress, isEnabled = prevState.isButtonEnabled(), + isHoldToConfirm = isHoldToConfirm, onClick = { if (isPrimaryButtonDisabled) { prevState.clickIntents.showPrimaryClickAlert() @@ -111,23 +113,27 @@ internal class SetButtonsStateTransformer( private fun StakingUiState.getConfirmationButtonText(): TextReference { val confirmationState = confirmationState as? StakingStates.ConfirmationState.Data + ?: return resourceReference(R.string.common_close) val amountState = amountState as? AmountState.Data - return 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) } + + return getBaseActionText(confirmationState) + ?: resourceReference(R.string.common_close) + } + + 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 StakingUiState.onPrimaryClick() { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt index a2f39e1550..bee0732784 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/SwapAmountComponentParams.kt @@ -24,7 +24,7 @@ internal sealed class SwapAmountComponentParams { abstract val primaryCryptoCurrencyStatusFlow: StateFlow abstract val secondaryCryptoCurrency: CryptoCurrency? abstract val filterProviderTypes: List - abstract val accountFlow: StateFlow + abstract val accountFlow: StateFlow abstract val isAccountModeFlow: StateFlow data class AmountParams( @@ -37,7 +37,7 @@ internal sealed class SwapAmountComponentParams { override val secondaryCryptoCurrency: CryptoCurrency?, override val filterProviderTypes: List = emptyList(), override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource, - override val accountFlow: StateFlow, + override val accountFlow: StateFlow, override val isAccountModeFlow: StateFlow, val title: TextReference, val callback: SwapAmountComponent.ModelCallback, @@ -54,7 +54,7 @@ internal sealed class SwapAmountComponentParams { override val secondaryCryptoCurrency: CryptoCurrency?, override val filterProviderTypes: List = emptyList(), override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource, - override val accountFlow: StateFlow, + override val accountFlow: StateFlow, override val isAccountModeFlow: StateFlow, val blockClickEnableFlow: StateFlow, ) : SwapAmountComponentParams() diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt index 9dd4198d6b..ae17646047 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt @@ -30,7 +30,7 @@ internal class SwapAmountFieldConverter( private val clickIntents: AmountScreenClickIntents, private val isSingleWallet: Boolean, private val isAccountsMode: Boolean, - private val account: Account.CryptoPortfolio?, + private val account: Account?, ) { private val iconStateConverter = CryptoCurrencyToIconStateConverter() diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt index 3febeaf504..56a7fab934 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt @@ -21,7 +21,7 @@ internal class SwapAmountBalanceHiddenTransformer( private val swapDirection: SwapDirection, private val clickIntents: AmountScreenClickIntents, private val isAccountsMode: Boolean, - private val account: Account.CryptoPortfolio?, + private val account: Account?, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt index ca25cc3fda..aa09d11164 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt @@ -27,7 +27,7 @@ internal class SwapAmountPrimaryReadyStateTransformer( private val isShowBestRateAnimation: Boolean, private val isSingleWallet: Boolean, private val isAccountsMode: Boolean, - private val account: Account.CryptoPortfolio?, + private val account: Account?, ) : Transformer { private val amountFieldConverter = SwapAmountFieldConverter( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt index 298b11ad75..f8f8600421 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt @@ -28,7 +28,7 @@ internal class SwapAmountSecondaryReadyStateTransformer( private val isShowBestRateAnimation: Boolean, private val isSingleWallet: Boolean, private val isAccountsMode: Boolean, - private val account: Account.CryptoPortfolio?, + private val account: Account?, ) : Transformer { private val amountFieldConverter = SwapAmountFieldConverter( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt index 5ce32201dc..e0fd2912f4 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt @@ -13,11 +13,12 @@ internal data class ConfirmData( val reduceAmountBy: BigDecimal, val isIgnoreReduce: Boolean, val enteredDestination: String?, + val enteredMemo: String?, val fee: Fee?, val feeError: GetFeeError?, val fromCryptoCurrencyStatus: CryptoCurrencyStatus?, val toCryptoCurrencyStatus: CryptoCurrencyStatus?, - val fromAccount: Account.CryptoPortfolio?, + val fromAccount: Account?, val quote: SwapQuoteUM?, val rateType: ExpressRateType?, ) \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt index cf27f09d02..8be7f8fc02 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt @@ -5,6 +5,9 @@ import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.swap.models.SwapDataModel +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import kotlinx.collections.immutable.ImmutableList @Immutable @@ -37,6 +40,9 @@ internal sealed class ConfirmUM { val txUrl: String, val swapDataModel: SwapDataModel, val provider: ExpressProvider, + val amountUM: SwapAmountUM, + val destinationUM: DestinationUM, + val feeSelectorUM: FeeSelectorUM, ) : ConfirmUM() data object Empty : ConfirmUM() { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt index 8de4dc6649..3634569cf9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt @@ -7,6 +7,8 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.domain.express.models.ExpressError import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.swap.v2.impl.notifications.model.SwapNotificationsModel import com.tangem.features.swap.v2.impl.notifications.ui.swapNotifications import kotlinx.collections.immutable.ImmutableList @@ -40,6 +42,10 @@ internal class SwapNotificationsComponent( data class SwapNotificationData( val expressError: ExpressError?, val fromCryptoCurrency: CryptoCurrency?, + val destinationAddress: String, + val memo: String? = null, + val toCryptoCurrencyStatus: CryptoCurrencyStatus? = null, + val userWalletId: UserWalletId? = null, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt index e2be8113cd..89e743866c 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt @@ -1,5 +1,6 @@ package com.tangem.features.swap.v2.impl.notifications.model +import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -7,6 +8,8 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase +import com.tangem.domain.utils.convertToSdkAmount import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger import com.tangem.features.swap.v2.impl.notifications.DefaultSwapNotificationsUpdateTrigger import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent @@ -14,6 +17,7 @@ import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateListener import com.tangem.features.swap.v2.impl.notifications.entity.SwapNotificationUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import java.math.BigDecimal import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -30,6 +34,7 @@ internal class SwapNotificationsModel @Inject constructor( private val swapNotificationsUpdateListener: SwapNotificationsUpdateListener, private val swapNotificationsUpdateTrigger: DefaultSwapNotificationsUpdateTrigger, private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger, + private val validateTransactionUseCase: ValidateTransactionUseCase, paramsContainer: ParamsContainer, ) : Model() { @@ -61,12 +66,33 @@ internal class SwapNotificationsModel @Inject constructor( private suspend fun buildNotifications() { val notifications = buildList { addExpressErrorNotification() + addDestinationTagRequiredNotification() } swapNotificationsUpdateTrigger.callbackHasError(notifications.isNotEmpty()) uiState.value = notifications.toImmutableList() } + private suspend fun MutableList.addDestinationTagRequiredNotification() { + val toCryptoCurrencyStatus = notificationData.toCryptoCurrencyStatus ?: return + val userWalletId = notificationData.userWalletId ?: return + val destinationAddress = notificationData.destinationAddress + if (destinationAddress.isEmpty()) return + + val validationError = validateTransactionUseCase( + amount = BigDecimal.ZERO.convertToSdkAmount(toCryptoCurrencyStatus), + fee = null, + memo = notificationData.memo, + destination = destinationAddress, + userWalletId = userWalletId, + network = toCryptoCurrencyStatus.currency.network, + ).leftOrNull() + + if (validationError is BlockchainSdkError.DestinationTagRequired) { + add(NotificationUM.Error.DestinationTagRequired) + } + } + fun MutableList.addExpressErrorNotification() { val expressError = notificationData.expressError ?: return val fromCryptoCurrency = notificationData.fromCryptoCurrency ?: return diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt index 300dd949eb..b6d57e6d09 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/DefaultSendWithSwapComponent.kt @@ -20,6 +20,7 @@ import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.account.Account import com.tangem.domain.swap.models.R import com.tangem.domain.swap.models.SwapDirection import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents @@ -110,8 +111,11 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor( } is SendWithSwapConfirmComponent -> { val fromCurrency = params.currency - val fromDerivationIndex = model.accountFlow.value?.derivationIndex?.value - .takeIf { model.isAccountModeFlow.value } + val fromDerivationIndex = when (val account = model.accountFlow.value) { + is Account.CryptoPortfolio -> account.derivationIndex.value + is Account.Payment -> TODO("[REDACTED_JIRA]") + null -> null + }.takeIf { model.isAccountModeFlow.value } analyticsEventHandler.send( CommonSendAnalyticEvents.ConfirmationScreenOpened( categoryName = model.analyticCategoryName, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt index 213c7d7b63..64e5650b5b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt @@ -3,6 +3,7 @@ package com.tangem.features.swap.v2.impl.sendviaswap.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FROM +import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_TO import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_BLOCKCHAIN @@ -24,6 +25,7 @@ internal sealed class SendWithSwapAnalyticEvents( val fromToken: CryptoCurrency, val toToken: CryptoCurrency, val fromDerivationIndex: Int?, + val toDerivationIndex: Int?, ) : SendWithSwapAnalyticEvents( event = "Send With Swap In Progress Screen Opened", params = buildMap { @@ -34,6 +36,7 @@ internal sealed class SendWithSwapAnalyticEvents( put(SEND_BLOCKCHAIN, fromToken.network.name) put(RECEIVE_BLOCKCHAIN, toToken.network.name) if (fromDerivationIndex != null) put(ACCOUNT_DERIVATION_FROM, fromDerivationIndex.toString()) + if (toDerivationIndex != null) put(ACCOUNT_DERIVATION_TO, toDerivationIndex.toString()) }, ), AppsFlyerIncludedEvent diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index 9bae584628..59d34c0701 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -133,6 +133,10 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( swapNotificationData = SwapNotificationsComponent.Params.SwapNotificationData( expressError = (model.confirmData.quote as? SwapQuoteUM.Error)?.expressError, fromCryptoCurrency = model.confirmData.fromCryptoCurrencyStatus?.currency, + destinationAddress = model.confirmData.enteredDestination.orEmpty(), + memo = model.confirmData.enteredMemo, + toCryptoCurrencyStatus = model.confirmData.toCryptoCurrencyStatus, + userWalletId = params.userWallet.walletId, ), ), ) @@ -181,7 +185,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( val isBalanceHidingFlow: StateFlow, val primaryCryptoCurrencyStatusFlow: StateFlow, val primaryFeePaidCurrencyStatusFlow: StateFlow, // doesn't change if select gasless fee - val accountFlow: StateFlow, + val accountFlow: StateFlow, val isAccountModeFlow: StateFlow, val callback: ModelCallback, ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 50666b4e1b..271decfaa7 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -21,9 +21,10 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCase import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.express.models.ExpressProviderType +import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.isHotWallet @@ -91,6 +92,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( private val swapNotificationsUpdateTrigger: SwapNotificationsUpdateTrigger, private val sendNotificationsUpdateListener: SendNotificationsUpdateListener, private val swapNotificationsUpdateListener: SwapNotificationsUpdateListener, + private val getAccountCurrencyByAddressUseCase: GetAccountCurrencyByAddressUseCase, private val swapAmountReduceTrigger: SwapAmountReduceTrigger, private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger, private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, @@ -142,6 +144,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( reduceAmountBy = amountState?.reduceAmountBy.takeIf { isQuoteContent }.orZero(), isIgnoreReduce = amountState?.isIgnoreReduce == true, enteredDestination = destinationUM?.addressTextField?.actualAddress, + enteredMemo = destinationUM?.memoTextField?.value, fee = feeSelectorUM?.selectedFeeItem?.fee.takeIf { isQuoteContent }, feeError = (uiState.value.feeSelectorUM as? FeeSelectorUM.Error)?.error.takeIf { isQuoteContent }, fromCryptoCurrencyStatus = amountUM?.swapDirection?.withSwapDirection( @@ -345,7 +348,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( txHash = txHash, currency = primaryCurrencyStatus.currency, ).getOrNull().orEmpty() - sendSuccessAnalytics() + modelScope.launch(dispatchers.default) { sendSuccessAnalytics() } uiState.transformerUpdate( SendWithSwapConfirmSentStateTransformer( timestamp = timestamp, @@ -426,6 +429,10 @@ internal class SendWithSwapConfirmModel @Inject constructor( data = SwapNotificationData( expressError = (confirmData.quote as? SwapQuoteUM.Error)?.expressError, fromCryptoCurrency = confirmData.fromCryptoCurrencyStatus?.currency, + destinationAddress = confirmData.enteredDestination.orEmpty(), + memo = confirmData.enteredMemo, + toCryptoCurrencyStatus = confirmData.toCryptoCurrencyStatus, + userWalletId = params.userWallet.walletId, ), ) uiState.transformerUpdate( @@ -452,13 +459,17 @@ internal class SendWithSwapConfirmModel @Inject constructor( }.launchIn(modelScope) } - private fun sendSuccessAnalytics() { + private suspend fun sendSuccessAnalytics() { val selectedProvider = confirmData.quote?.provider ?: return val fromCurrency = confirmData.fromCryptoCurrencyStatus?.currency ?: return val toCurrency = confirmData.toCryptoCurrencyStatus?.currency ?: return val feeSelectorUM = uiState.value.feeSelectorUM as? FeeSelectorUM.Content ?: return val feeType = feeSelectorUM.toAnalyticType() val fromDerivationIndex = confirmData.fromAccount?.derivationIndex?.value + val destination = destinationUM?.addressTextField?.actualAddress ?: return + val destinationAccount = getAccountCurrencyByAddressUseCase(destination) + .getOrNull()?.account + val toDerivationIndex = destinationAccount?.derivationIndex?.value analyticsEventHandler.send( SendWithSwapAnalyticEvents.TransactionScreenOpened( @@ -467,6 +478,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( fromToken = fromCurrency, toToken = toCurrency, fromDerivationIndex = fromDerivationIndex, + toDerivationIndex = toDerivationIndex, ), ) analyticsEventHandler.send( @@ -541,10 +553,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( private fun getPrimaryButtonText(confirmUM: ConfirmUM, isHoldToConfirm: Boolean): TextReference { return when { - isHoldToConfirm -> resourceReference( - id = com.tangem.core.ui.R.string.common_hold_to, - formatArgs = wrappedList(resourceReference(R.string.common_send)), - ) + isHoldToConfirm -> resourceReference(R.string.common_send) confirmUM is ConfirmUM.Success -> resourceReference(R.string.common_close) confirmUM is ConfirmUM.Content && confirmUM.isTransactionInProcess -> resourceReference(R.string.send_sending) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt index 50a94cdb36..3618110f82 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SwapTransactionSender.kt @@ -104,6 +104,7 @@ internal class SwapTransactionSender @AssistedInject constructor( fromAmount = fromAmount.toStringWithRightOffset(fromStatus.currency.decimals), toCryptoCurrency = toStatus.currency, toAddress = destination, + toExtraId = confirmData.enteredMemo, expressProvider = provider, rateType = rateType, expressOperationType = expressOperationType, @@ -128,7 +129,7 @@ internal class SwapTransactionSender @AssistedInject constructor( fromAmount: BigDecimal, fromStatus: CryptoCurrencyStatus, toStatus: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, fee: Fee, feeExtended: TransactionFeeExtended?, provider: ExpressProvider, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmSentStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmSentStateTransformer.kt index 970c3f1f93..d0ade6d256 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmSentStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmSentStateTransformer.kt @@ -20,6 +20,9 @@ internal class SendWithSwapConfirmSentStateTransformer( txUrl = txUrl, provider = provider, swapDataModel = swapDataModel, + amountUM = prevState.amountUM, + destinationUM = prevState.destinationUM, + feeSelectorUM = prevState.feeSelectorUM, ), ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt index 629d37898d..31b4ba8651 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt @@ -96,7 +96,7 @@ internal class SendWithSwapModel @Inject constructor( ), ) - val accountFlow: StateFlow + val accountFlow: StateFlow field = MutableStateFlow(null) val isAccountModeFlow: StateFlow diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt index 44ca767ac2..834289a971 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt @@ -93,10 +93,10 @@ internal fun SendWithSwapSuccessContent(sendWithSwapUM: SendWithSwapUM) { @Composable private fun SuccessContent(sendWithSwapUM: SendWithSwapUM, modifier: Modifier = Modifier) { val confirmUM = sendWithSwapUM.confirmUM as? ConfirmUM.Success ?: return - val amountUM = sendWithSwapUM.amountUM as? SwapAmountUM.Content ?: return + val amountUM = confirmUM.amountUM as? SwapAmountUM.Content ?: return val quoteUM = amountUM.selectedQuote as? SwapQuoteUM.Content ?: return - val destinationUM = sendWithSwapUM.destinationUM as? DestinationUM.Content ?: return - val feeSelectorUM = sendWithSwapUM.feeSelectorUM as? FeeSelectorUM.Content ?: return + val destinationUM = confirmUM.destinationUM as? DestinationUM.Content ?: return + val feeSelectorUM = confirmUM.feeSelectorUM as? FeeSelectorUM.Content ?: return Column( modifier = modifier @@ -125,7 +125,10 @@ private fun SuccessContent(sendWithSwapUM: SendWithSwapUM, modifier: Modifier = .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action), ) - DestinationBlock(destinationUM.addressTextField) + DestinationBlock( + address = destinationUM.addressTextField, + memo = destinationUM.memoTextField, + ) FeeBlock(feeSelectorUM = feeSelectorUM) Spacer(Modifier.height(16.dp)) } @@ -247,7 +250,11 @@ private fun FeeBlock(feeSelectorUM: FeeSelectorUM.Content) { } @Composable -private fun DestinationBlock(address: DestinationTextFieldUM.RecipientAddress, modifier: Modifier = Modifier) { +private fun DestinationBlock( + address: DestinationTextFieldUM.RecipientAddress, + memo: DestinationTextFieldUM.RecipientMemo?, + modifier: Modifier = Modifier, +) { Column( modifier = modifier .fillMaxWidth() @@ -279,6 +286,14 @@ private fun DestinationBlock(address: DestinationTextFieldUM.RecipientAddress, m .background(TangemTheme.colors.background.tertiary), ) } + if (memo != null && memo.value.isNotBlank()) { + Text( + text = stringResourceSafe(R.string.send_memo, memo.value), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + ) + } } } @@ -304,7 +319,17 @@ private fun SendWithSwapSuccessContent_Preview() { isValuePasted = false, blockchainAddress = "0x391316d97a07027a0702c8A002c8A0C25d8470", ), - memoTextField = null, + memoTextField = DestinationTextFieldUM.RecipientMemo( + value = "123123123", + keyboardOptions = KeyboardOptions(), + placeholder = TextReference.EMPTY, + label = resourceReference(R.string.send_recipient), + isError = false, + error = null, + isValuePasted = false, + isEnabled = true, + disabledText = TextReference.EMPTY, + ), recent = persistentListOf(), wallets = persistentListOf(), networkName = "Polygon", @@ -368,6 +393,64 @@ private fun SendWithSwapSuccessContent_Preview() { txExtraIdName = "Jeffry Blackwell", ), ), + amountUM = SwapAmountContentPreview.defaultState, + destinationUM = DestinationUM.Content( + isPrimaryButtonEnabled = false, + addressTextField = DestinationTextFieldUM.RecipientAddress( + value = "0x391316d97a07027a0702c8A002c8A0C25d8470", + keyboardOptions = KeyboardOptions(), + placeholder = TextReference.EMPTY, + label = resourceReference(R.string.send_recipient), + isError = false, + error = null, + isValuePasted = false, + blockchainAddress = "0x391316d97a07027a0702c8A002c8A0C25d8470", + ), + memoTextField = DestinationTextFieldUM.RecipientMemo( + value = "123123123", + keyboardOptions = KeyboardOptions(), + placeholder = TextReference.EMPTY, + label = resourceReference(R.string.send_recipient), + isError = false, + error = null, + isValuePasted = false, + isEnabled = true, + disabledText = TextReference.EMPTY, + ), + recent = persistentListOf(), + wallets = persistentListOf(), + networkName = "Polygon", + isValidating = false, + isInitialized = false, + isRecentHidden = false, + isAccountsMode = false, + ), + feeSelectorUM = FeeSelectorUM.Content( + fees = TransactionFee.Single( + normal = Fee.Common( + BigDecimal.ONE.convertToSdkAmount( + SwapAmountContentPreview.cryptoCurrencyStatus, + ), + ), + ), + feeItems = persistentListOf(), + selectedFeeItem = FeeItem.Market( + Fee.Common( + BigDecimal.ONE.convertToSdkAmount( + SwapAmountContentPreview.cryptoCurrencyStatus, + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + feeCryptoCurrencyStatus = SwapAmountContentPreview.cryptoCurrencyStatus, + ), + feeFiatRateUM = null, + feeNonce = FeeNonce.None, + isPrimaryButtonEnabled = false, + ), ), navigationUM = NavigationUM.Content( source = SendWithSwapRoute.Success.javaClass.simpleName, diff --git a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt index d2ef17de45..ed9144cac3 100644 --- a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt +++ b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt @@ -18,7 +18,7 @@ interface SwapComponent : ComposableContentComponent { val screenSource: String, val tangemPayInput: TangemPayInput? = null, val preselectedToToken: CryptoCurrencyStatus? = null, - val preselectedAccount: Account.CryptoPortfolio? = null, + val preselectedAccount: Account? = null, ) { data class TangemPayInput( val cryptoAmount: BigDecimal, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index 2b464a5d59..7e3ef9caeb 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -9,9 +9,7 @@ import com.squareup.moshi.Moshi import com.tangem.blockchain.common.Approver import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token -import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow @@ -25,7 +23,6 @@ import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.exchangeservice.swap.ExpressUtils import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.models.currency.CryptoCurrency @@ -40,6 +37,7 @@ import com.tangem.feature.swap.domain.models.createFromAmountWithOffset import com.tangem.feature.swap.domain.models.domain.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async +import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.withContext import timber.log.Timber import java.io.IOException @@ -52,19 +50,16 @@ internal class DefaultSwapRepository( private val tangemExpressApi: TangemExpressApi, private val coroutineDispatcher: CoroutineDispatcherProvider, private val walletManagersFacade: WalletManagersFacade, - private val userWalletsStore: UserWalletsStore, private val errorsDataConverter: ErrorsDataConverter, private val dataSignatureVerifier: DataSignatureVerifier, private val appPreferencesStore: AppPreferencesStore, private val rampStateManager: RampStateManager, moshi: Moshi, - excludedBlockchains: ExcludedBlockchains, ) : SwapRepository { private val expressDataConverter = ExpressDataConverter() private val leastTokenInfoConverter = LeastTokenInfoConverter() private val swapPairInfoConverter = SwapPairInfoConverter() - private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) private val exchangeStatusConverter = ExchangeStatusConverter() private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java) @@ -130,21 +125,48 @@ internal class DefaultSwapRepository( userWallet: UserWallet, initialCurrency: LeastTokenInfo, currencyList: List, + isIgnoreExpress: Boolean, ): PairsWithProviders { return withContext(coroutineDispatcher.io) { - try { - val initial = NetworkLeastTokenInfo( - contractAddress = initialCurrency.contractAddress, - network = initialCurrency.network, - ) - val currenciesList = currencyList - .filter { currency -> - val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, currency) - val isAvailableForSwap = rampStateManager.checkAssetRequirements(requirements) - isAvailableForSwap - } - .map { currency -> leastTokenInfoConverter.convert(currency) } + val currenciesList = filterByAssetRequirements(userWallet, currencyList) + if (isIgnoreExpress) { + buildLocalPairs(initialCurrency, currenciesList) + } else { + fetchExpressPairs(userWallet, initialCurrency, currenciesList) + } + } + } + + private fun buildLocalPairs( + initialCurrency: LeastTokenInfo, + currenciesList: List, + ): PairsWithProviders { + val pairs = currenciesList.map { tokenInfo -> + SwapPairLeast( + from = initialCurrency, + to = LeastTokenInfo( + contractAddress = tokenInfo.contractAddress, + network = tokenInfo.network, + ), + providers = emptyList(), + ) + } + return PairsWithProviders(pairs = pairs, allProviders = emptyList()) + } + + private suspend fun fetchExpressPairs( + userWallet: UserWallet, + initialCurrency: LeastTokenInfo, + currenciesList: List, + ): PairsWithProviders { + try { + val initial = NetworkLeastTokenInfo( + contractAddress = initialCurrency.contractAddress, + network = initialCurrency.network, + ) + + val allPairs = supervisorScope { val pairsDeferred = async { getPairsInternal( userWallet = userWallet, @@ -161,27 +183,36 @@ internal class DefaultSwapRepository( ) } - val pairs = pairsDeferred.await().getOrThrow() - val reversedPairs = reversedPairsDeferred.await().getOrThrow() + pairsDeferred.await().getOrThrow() + reversedPairsDeferred.await().getOrThrow() + } - val allPairs = pairs + reversedPairs - - return@withContext swapPairInfoConverter.convert( - SwapPairsWithProviders( - swapPair = allPairs, - providers = emptyList(), - ), - ) - } catch (exception: Exception) { - if (exception is ApiResponseError.HttpException) { - throw ExpressException(errorsDataConverter.convert(exception.errorBody.orEmpty())) - } else { - throw exception - } + return swapPairInfoConverter.convert( + SwapPairsWithProviders( + swapPair = allPairs, + providers = emptyList(), + ), + ) + } catch (exception: Exception) { + if (exception is ApiResponseError.HttpException) { + throw ExpressException(errorsDataConverter.convert(exception.errorBody.orEmpty())) + } else { + throw exception } } } + private suspend fun filterByAssetRequirements( + userWallet: UserWallet, + currencyList: List, + ): List { + return currencyList + .filter { currency -> + val requirements = walletManagersFacade.getAssetRequirements(userWallet.walletId, currency) + rampStateManager.checkAssetRequirements(requirements) + } + .map { currency -> leastTokenInfoConverter.convert(currency) } + } + private suspend fun getPairsInternal( userWallet: UserWallet, from: List, @@ -286,6 +317,7 @@ internal class DefaultSwapRepository( expressOperationType: ExpressOperationType, refundAddress: String?, // for cex only refundExtraId: String?, // for cex only + toExtraId: String?, // for networks with memo only ): Either { return withContext(coroutineDispatcher.io) { try { @@ -311,6 +343,7 @@ internal class DefaultSwapRepository( userWallet = userWallet, appPreferencesStore = appPreferencesStore, ), + toExtraId = toExtraId?.ifEmpty { null }, ).getOrThrow() if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) { val txDetails = parseTxDetails(response.txDetailsJson) @@ -406,18 +439,6 @@ internal class DefaultSwapRepository( ) } - override fun getNativeTokenForNetwork(networkId: String): CryptoCurrency { - val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - - return requireNotNull( - cryptoCurrencyFactory.createCoin( - blockchain = blockchain, - extraDerivationPath = null, - userWallet = requireNotNull(userWalletsStore.selectedUserWalletOrNull), - ), - ) - } - private fun getDataError(ex: Exception): ExpressDataError { return if (ex is ApiResponseError.HttpException) { errorsDataConverter.convert(ex.errorBody.orEmpty()) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt index fb7b9d0ed1..a04749322a 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt @@ -47,8 +47,8 @@ internal class DefaultSwapTransactionRepository( userWalletId: UserWalletId, fromCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, transaction: SavedSwapTransactionModel, ) { transaction.status?.let { status -> @@ -248,8 +248,8 @@ internal class DefaultSwapTransactionRepository( userWalletId: UserWalletId, fromCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, transactions: List, ): List { return addOrReplace( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt index 5c0b55b780..2b50f90d14 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt @@ -120,8 +120,8 @@ internal class SavedSwapTransactionListConverter( userWalletId: UserWalletId, fromCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, tokenTransactions: List, ) = SavedSwapTransactionListModelInner( userWalletId = userWalletId.stringValue, @@ -161,10 +161,7 @@ internal class SavedSwapTransactionListConverter( } } - private fun findAccountByDerivationIndex( - accountList: AccountList?, - derivationIndex: DerivationIndex?, - ): Account.CryptoPortfolio? { + private fun findAccountByDerivationIndex(accountList: AccountList?, derivationIndex: DerivationIndex?): Account? { return accountList?.accounts?.asSequence()?.filterIsInstance() ?.firstOrNull { it.derivationIndex == derivationIndex } } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index a64fc2cf07..414a2649d7 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -25,9 +25,9 @@ class SwapPairInfoConverter : Converter - val filteredCurrencies = accountStatus.flattenCurrencies().filter { status -> - val isDifferentCurrency = status.currency.network.backendId != currency.network.backendId || - status.currency.getContractAddress() != currency.getContractAddress() - - val hasValidStatus = - status.value is CryptoCurrencyStatus.Loaded || status.value is CryptoCurrencyStatus.NoAccount - val isNotCustomToken = !status.currency.isCustom - - hasValidStatus && isDifferentCurrency && isNotCustomToken + val walletAccountCurrencyStatusesExceptInitial: Map> = + walletAccountCurrencyStatuses.mapNotNull { accountStatus -> + val filteredCurrencies = when (accountStatus) { + is AccountStatus.CryptoPortfolio -> accountStatus.flattenCurrencies().filterCurrencies(currency) + is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") } if (filteredCurrencies.isNotEmpty()) { @@ -234,6 +230,17 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } + private fun List.filterCurrencies(currency: CryptoCurrency) = this.filter { status -> + val isDifferentCurrency = status.currency.network.backendId != currency.network.backendId || + status.currency.getContractAddress() != currency.getContractAddress() + + val hasValidStatus = + status.value is CryptoCurrencyStatus.Loaded || status.value is CryptoCurrencyStatus.NoAccount + val isNotCustomToken = !status.currency.isCustom + + hasValidStatus && isDifferentCurrency && isNotCustomToken + } + private suspend fun getToCurrenciesGroup( currency: CryptoCurrency, leastPairs: List, @@ -293,7 +300,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val isUnavailable = providers.isNullOrEmpty() AccountSwapCurrency( isAvailable = !isUnavailable, - account = cryptoPortfolio, + account = accountEntry, cryptoCurrencyStatus = currencyStatus, providers = providers.orEmpty(), ) @@ -400,7 +407,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( """ Find the best quote |- fromToken: $fromToken + |- fromAccount: $fromAccount |- toToken: $toToken + |- toAccount: $toAccount |- providers: $providers |- amountToSwap: $amountToSwap |- selectedFee: $txFeeSealedState @@ -767,8 +776,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapData: SwapDataModel?, currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, amountToSwap: String, includeFeeInAmount: IncludeFeeInAmount, fee: TxFee?, @@ -848,8 +857,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapData: SwapDataModel, currencyToSendStatus: CryptoCurrencyStatus, currencyToGetStatus: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, amountToSwap: String, txFee: TxFee, ): SwapTransactionState { @@ -890,8 +899,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapData: SwapDataModel, currencyToSendStatus: CryptoCurrencyStatus, currencyToGetStatus: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, amountToSwap: String, ): SwapTransactionState { val dexTransaction = swapData.transaction as? ExpressTransactionModel.DEX @@ -919,8 +928,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapData: SwapDataModel, currencyToSendStatus: CryptoCurrencyStatus, currencyToGetStatus: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, amount: SwapAmount, txData: TransactionData, payInAddress: String, @@ -988,8 +997,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun onSwapCex( currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, amount: SwapAmount, txFee: TxFee?, swapProvider: SwapProvider, @@ -1022,6 +1031,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( exchangeData.transaction as? ExpressTransactionModel.CEX ?: return SwapTransactionState.Error.UnknownError if (isTangemPayWithdrawal) { + val networkAddress = currencyToSend.value.networkAddress return SwapTransactionState.TangemPayWithdrawalData( cryptoAmount = amount.value, cryptoCurrencyId = requireNotNull(currencyToSend.currency.id.rawCurrencyId), @@ -1048,6 +1058,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( txExternalId = exchangeDataCex.externalTxId, averageDuration = null, ), + exchangeData = TangemPayWithdrawExchangeState( + txId = exchangeDataCex.txId, + fromNetwork = currencyToSend.currency.network.backendId, + fromAddress = networkAddress?.defaultAddress?.value.orEmpty(), + payInAddress = exchangeData.transaction.txTo, + payInExtraId = exchangeDataCex.txExtraId, + ), ) } @@ -1150,8 +1167,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( override suspend fun storeSwapTransaction( currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, amount: SwapAmount, swapProvider: SwapProvider, swapDataModel: SwapDataModel, @@ -1187,9 +1204,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( @Suppress("LongParameterList") override suspend fun loadFeeForSwapTransaction( fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, amount: String, reduceBalanceBy: BigDecimal, provider: SwapProvider, @@ -1225,9 +1242,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( override suspend fun loadFeeForSwapTransaction( fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, amount: String, reduceBalanceBy: BigDecimal, provider: SwapProvider, @@ -1338,8 +1355,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( } } - override fun getNativeToken(networkId: String): CryptoCurrency { - return repository.getNativeTokenForNetwork(networkId) + override suspend fun getNativeToken(network: Network): CryptoCurrency { + return multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), + ) + ?.filterIsInstance() + ?.firstOrNull { it.network.id == network.id && it.network.derivationPath == network.derivationPath } + ?: error("Unable to create network coin with ID: ${network.id}") } private suspend fun isAllowedToSpend( @@ -1488,7 +1510,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( return quoteDataModel.fold( ifRight = { quoteModel -> val swapState = updateBalances( - networkId = networkId, fromTokenStatus = fromToken, fromAccount = fromAccount, toTokenStatus = toToken, @@ -1736,10 +1757,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( } else -> { if (feeValue < amount.value) { + val nativeCoinDecimals = Blockchain.fromNetworkId(networkId)?.decimals() + ?: error("Blockchain not found") IncludeFeeInAmount.Included( amountSubtractFee = SwapAmount( reducedBalance - feeValue, - getNativeToken(fromToken.network.backendId).decimals, + nativeCoinDecimals, ), ) } else { @@ -1756,7 +1779,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) val feeCurrencyId: CryptoCurrency.ID = when (feePaidCurrency) { is FeePaidCurrency.Token -> feePaidCurrency.tokenId - else -> getNativeToken(networkId = fromToken.network.backendId).id + else -> getNativeToken(network = fromToken.network).id } val rates = getQuotes(feeCurrencyId) return rates[feeCurrencyId]?.let { rate -> @@ -1852,7 +1875,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( includeFeeInAmount = includeFeeInAmount, ) val swapState = updateBalances( - networkId = networkId, fromTokenStatus = fromToken, fromAccount = fromAccount, toTokenStatus = toToken, @@ -2020,7 +2042,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( @Suppress("LongParameterList", "MaxChainedCallsOnSameLine") private suspend fun updateBalances( provider: SwapProvider, - networkId: String, fromTokenStatus: CryptoCurrencyStatus, fromAccount: Account.CryptoPortfolio?, toTokenStatus: CryptoCurrencyStatus, @@ -2032,7 +2053,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( ): SwapState.QuotesLoadedState { val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency - val nativeToken = repository.getNativeTokenForNetwork(networkId) + val nativeToken = getNativeToken(fromToken.network) val rates = getQuotes(fromToken.id, toToken.id, nativeToken.id) return SwapState.QuotesLoadedState( fromTokenInfo = TokenSwapInfo( @@ -2461,7 +2482,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( if (balanceToCheck > fee.multiply(percentsToFeeIncrease)) { SwapFeeState.Enough } else { - val nativeToken = getNativeToken(fromTokenStatus.currency.network.backendId) + val nativeToken = getNativeToken(fromTokenStatus.currency.network) SwapFeeState.NotEnough( feeCurrency = nativeToken, currencyName = nativeToken.network.name, @@ -2505,11 +2526,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( } } is FeePaidCurrency.FeeResource -> { - val network = repository.getNativeTokenForNetwork(networkId).network val isFeeResourceEnough = currencyChecksRepository.checkIfFeeResourceEnough( amount = spendAmount.value, userWalletId = userWalletId, - network = network, + network = fromTokenStatus.currency.network, ) if (isFeeResourceEnough) { @@ -2553,7 +2573,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val areAllQuotesFound = cachedQuotes?.all { quote -> quote.value !is QuoteStatus.Empty } == true - if (areAllQuotesFound) return@runSuspendCatching cachedQuotes.orEmpty() + if (areAllQuotesFound) return@runSuspendCatching cachedQuotes val currenciesIds = if (cachedQuotes.isNullOrEmpty()) { this@getQuotesOrEmpty @@ -2612,11 +2632,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( sealed class TxFeeSealedState { class Legacy(val txFeeState: TxFeeState, val selectedFee: FeeType) : TxFeeSealedState() class Component(val txFee: TxFee.FeeComponent) : TxFeeSealedState() - - fun getTxFeeStateOrNull() = when (this) { - is Component -> null - is Legacy -> txFeeState - } } sealed class TransactionFeeResult { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt index 122d227590..0720aa7abf 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt @@ -17,8 +17,8 @@ interface SwapTransactionRepository { userWalletId: UserWalletId, fromCryptoCurrency: CryptoCurrency, toCryptoCurrency: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, transaction: SavedSwapTransactionModel, ) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt index fedf814baa..c9ba429d75 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt @@ -6,12 +6,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.swap.domain.models.ExpressDataError -import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel -import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo -import com.tangem.feature.swap.domain.models.domain.PairsWithProviders -import com.tangem.feature.swap.domain.models.domain.QuoteModel -import com.tangem.feature.swap.domain.models.domain.RateType -import com.tangem.feature.swap.domain.models.domain.SwapDataModel +import com.tangem.feature.swap.domain.models.domain.* import java.math.BigDecimal interface SwapRepository { @@ -27,6 +22,7 @@ interface SwapRepository { userWallet: UserWallet, initialCurrency: LeastTokenInfo, currencyList: List, + isIgnoreExpress: Boolean = false, ): PairsWithProviders suspend fun getExchangeStatus(userWallet: UserWallet, txId: String): Either @@ -73,6 +69,7 @@ interface SwapRepository { expressOperationType: ExpressOperationType, refundAddress: String? = null, // for cex only refundExtraId: String? = null, // for cex only + toExtraId: String? = null, // for networks with memo only ): Either // TODO: Add target error handling, remove either ([REDACTED_JIRA]) @@ -86,6 +83,4 @@ interface SwapRepository { txHash: String, payInExtraId: String?, ): Either - - fun getNativeTokenForNetwork(networkId: String): CryptoCurrency } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt index f25e4b1c1a..31236c6313 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt @@ -13,8 +13,8 @@ data class SavedSwapTransactionListModel( val toCryptoCurrencyId: String, val fromCryptoCurrency: CryptoCurrency, val toCryptoCurrency: CryptoCurrency, - val fromAccount: Account.CryptoPortfolio?, - val toAccount: Account.CryptoPortfolio?, + val fromAccount: Account?, + val toAccount: Account?, val transactions: List, ) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt index bf97fe58d9..f65dd60a98 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt @@ -53,6 +53,8 @@ data class SwapProvider( val isRecommended: Boolean = false, @Json(name = "slippage") val slippage: BigDecimal?, + @Json(name = "isExtraIdSupported") + val isExtraIdSupported: Boolean = false, ) @JsonClass(generateAdapter = false) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt index 7ad787a146..b1380f6a3a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapTransactionState.kt @@ -3,6 +3,7 @@ package com.tangem.feature.swap.domain.models.ui import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.pay.TangemPayWithdrawExchangeState import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount @@ -31,13 +32,14 @@ sealed class SwapTransactionState { val toAmount: String?, val toAmountValue: BigDecimal?, val storeData: StoreTransactionData, + val exchangeData: TangemPayWithdrawExchangeState, ) : SwapTransactionState() { data class StoreTransactionData( val currencyToSend: CryptoCurrencyStatus, val currencyToGet: CryptoCurrencyStatus, - val fromAccount: Account.CryptoPortfolio?, - val toAccount: Account.CryptoPortfolio?, + val fromAccount: Account?, + val toAccount: Account?, val amount: SwapAmount, val swapProvider: SwapProvider, val swapDataModel: SwapDataModel, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 98955f6373..89a8266fd1 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -25,7 +25,7 @@ sealed class SwapEvents( params: Map = emptyMap(), ) : AnalyticsEvent(SWAP_CATEGORY, event, params) { - data class SwapScreenOpened( + class SwapScreenOpened( val token: String, val blockchain: String, ) : SwapEvents( @@ -38,12 +38,15 @@ sealed class SwapEvents( class SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") - data class ChooseTokenScreenOpened(val hasAvailableTokens: Boolean) : SwapEvents( + class ChooseTokenScreenOpened(val hasAvailableTokens: Boolean) : SwapEvents( event = "Choose Token Screen Opened", params = mapOf("Available tokens" to if (hasAvailableTokens) "Yes" else "No"), ) - data class ChooseTokenScreenResult(val isTokenChosen: Boolean, val token: String? = null) : SwapEvents( + class ChooseTokenScreenResult( + val isTokenChosen: Boolean, + val token: String? = null, + ) : SwapEvents( event = "Choose Token Screen Result", params = buildMap { put("Token Chosen", if (isTokenChosen) "Yes" else "No") @@ -51,12 +54,12 @@ sealed class SwapEvents( }, ) - data class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents( + class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents( event = "Button - Swap", params = mapOf("Send Token" to sendToken, "Receive Token" to receiveToken), ) - data class ButtonGivePermissionClicked( + class ButtonGivePermissionClicked( val sendToken: String, val receiveToken: String, val provider: SwapProvider, @@ -69,7 +72,7 @@ sealed class SwapEvents( ), ) - data class ButtonPermissionApproveClicked( + class ButtonPermissionApproveClicked( val sendToken: String, val receiveToken: String, val approveType: ApproveType, @@ -88,8 +91,8 @@ sealed class SwapEvents( class ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") - @Suppress("NullableToStringCall") - data class SwapInProgressScreen( + @Suppress("NullableToStringCall", "LongParameterList") + class SwapInProgressScreen( val provider: SwapProvider, val commission: FeeType, // Market / Fast val sendBlockchain: String, @@ -118,24 +121,39 @@ sealed class SwapEvents( class ProviderClicked : SwapEvents("Provider Clicked") - data class ProviderChosen(val provider: SwapProvider) : SwapEvents( + class ProviderChosen(val provider: SwapProvider) : SwapEvents( event = "Provider Chosen", params = mapOf("Provider" to provider.name), ) - data class ButtonStatus(val token: String) : SwapEvents( + class ButtonStatus(val token: String) : SwapEvents( event = "Button - Status", params = mapOf("Token" to token), ) - data class ButtonExplore(val token: String) : SwapEvents( + class ButtonExplore(val token: String) : SwapEvents( event = "Button - Explore", params = mapOf("Token" to token), ) class NoticeNoAvailableTokensToSwap : SwapEvents("Notice - No Available Tokens To Swap") - data class NoticeNotEnoughFee(val token: String, val blockchain: String) : SwapEvents( + class NoticeUnavailableToSwapPair( + val sendToken: String, + val receiveToken: String, + val sendBlockchain: String, + val receiveBlockchain: String, + ) : SwapEvents( + event = "Notice - Unavailable To Swap Pair", + params = mapOf( + SEND_TOKEN to sendToken, + RECEIVE_TOKEN to receiveToken, + "Send Blockchain" to sendBlockchain, + "Receive Blockchain" to receiveBlockchain, + ), + ) + + class NoticeNotEnoughFee(val token: String, val blockchain: String) : SwapEvents( event = "Notice - Not Enough Fee", params = mapOf( "Token" to token, @@ -143,7 +161,7 @@ sealed class SwapEvents( ), ) - data class NoticeProviderError( + class NoticeProviderError( val sendToken: String, val receiveToken: String, val provider: SwapProvider, @@ -162,7 +180,7 @@ sealed class SwapEvents( // TODO parameters // region Promo activity - data class ChangellyActivity( + class ChangellyActivity( val promoState: PromoState, ) : AnalyticsEvent( category = PROMO_CATEGORY, @@ -177,7 +195,7 @@ sealed class SwapEvents( } } - data class NoticePermissionNeeded( + class NoticePermissionNeeded( val sendToken: String, val receiveToken: String, val provider: SwapProvider, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/component/SwapFeeSelectorBlockComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/component/SwapFeeSelectorBlockComponent.kt index 19e1760238..96a5d81ee3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/component/SwapFeeSelectorBlockComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/component/SwapFeeSelectorBlockComponent.kt @@ -20,6 +20,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -55,7 +56,11 @@ class SwapFeeSelectorBlockComponent @AssistedInject constructor( init { params.repository.state - .onEach(feeSelectorBlockComponent::updateState) + .onEach { feeSelectorBlockComponent.updateState(it) } + .launchIn(componentScope) + + params.repository.forceUpdateState + .onEach { feeSelectorBlockComponent.updateState(it) } .launchIn(componentScope) } @@ -68,6 +73,9 @@ class SwapFeeSelectorBlockComponent @AssistedInject constructor( val state: StateFlow get() = MutableStateFlow(FeeSelectorUM.Loading) + val forceUpdateState: SharedFlow + get() = MutableStateFlow(FeeSelectorUM.Loading) + fun onResult(newState: FeeSelectorUM) suspend fun loadFee(): Either diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt index 4315d780ac..919e908498 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt @@ -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 @@ -33,11 +36,8 @@ internal class AccountTokenItemConverter( isExpanded = true, isCollapsable = false, tokens = value.currencyList.map { accountSwapCurrency -> - if (accountSwapCurrency.isAvailable) { - createAvailableItemConverter() - } else { - createUnavailableItemConverter() - }.convert(accountSwapCurrency.cryptoCurrencyStatus) + createAvailableItemConverter() + .convert(accountSwapCurrency.cryptoCurrencyStatus) }.map(TokensListItemUM::Token).toPersistentList(), ) } @@ -108,7 +108,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 +134,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/converters/SwapTransactionErrorStateConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt index 646e3a3218..dc6c9cfbb0 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt @@ -17,7 +17,7 @@ internal class SwapTransactionErrorStateConverter( is SwapTransactionState.Error.TransactionError -> { when (val error = value.error) { is SendTransactionError.UserCancelledError -> return null - null -> SwapAlertUM.GenericError(onDismiss) + null -> SwapAlertUM.DefaultError(onDismiss) else -> TransactionErrorAlertConverter(onDismiss, onSupportClick).convert(error) } } @@ -27,8 +27,8 @@ internal class SwapTransactionErrorStateConverter( onConfirmClick = { onSupportClick(value.error.code.toString()) }, ) } - SwapTransactionState.Error.UnknownError -> SwapAlertUM.GenericError(onDismiss) - is SwapTransactionState.Error.TangemPayWithdrawalError -> SwapAlertUM.GenericError( + SwapTransactionState.Error.UnknownError -> SwapAlertUM.DefaultError(onDismiss) + is SwapTransactionState.Error.TangemPayWithdrawalError -> SwapAlertUM.SupportError( onConfirmClick = { onSupportClick(value.txId) }, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index ba936978ec..791af35a8c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -17,6 +17,7 @@ import com.tangem.feature.swap.models.TokenToSelectState import com.tangem.feature.swap.presentation.R import com.tangem.utils.Provider import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList internal class TokensDataConverter( @@ -31,14 +32,9 @@ internal class TokensDataConverter( val availableTitle = TokenToSelectState.Title( resourceReference(R.string.exchange_tokens_available_tokens_header), ) - val unavailableTitle = TokenToSelectState.Title( - resourceReference( - R.string.exchange_tokens_unavailable_tokens_header, - wrappedList(value.fromCurrency.name), - ), - ) + val allTokens = group.available + group.unavailable return SwapSelectTokenStateHolder( - availableTokens = group.available.map { tokenWithBalanceToTokenToSelect(it, true) } + availableTokens = allTokens.map { tokenWithBalanceToTokenToSelect(it, true) } .toMutableList() .apply { if (this.isNotEmpty()) { @@ -46,14 +42,7 @@ internal class TokensDataConverter( } } .toImmutableList(), - unavailableTokens = group.unavailable.map { tokenWithBalanceToTokenToSelect(it, false) } - .toMutableList() - .apply { - if (this.isNotEmpty()) { - this.add(0, unavailableTitle) - } - } - .toImmutableList(), + unavailableTokens = persistentListOf(), tokensListData = TokenListUMData.EmptyList, onSearchEntered = onSearchEntered, onTokenSelected = onTokenSelected, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt index af538747a6..668b30aa88 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt @@ -46,11 +46,8 @@ internal class TokensDataConverterV2( } else { val tokensList = accountList.flatMap { (_, currencyList) -> currencyList.asSequence().map { accountSwapCurrency -> - if (accountSwapCurrency.isAvailable) { - accountListItemConverter.createAvailableItemConverter() - } else { - accountListItemConverter.createUnavailableItemConverter() - }.convert(accountSwapCurrency.cryptoCurrencyStatus) + accountListItemConverter.createAvailableItemConverter() + .convert(accountSwapCurrency.cryptoCurrencyStatus) }.map(TokensListItemUM::Token).toPersistentList() }.toPersistentList() diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 1448ec5de8..ab7b88c30d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -18,12 +18,15 @@ import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.models.event.SwapAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles +import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.InputNumberFormatter @@ -43,7 +46,9 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase -import com.tangem.domain.markets.GetTokenMarketInfoUseCase +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenMarketListConfig +import com.tangem.domain.markets.toSerializableParam import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -73,7 +78,6 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.swap.analytics.StoriesEvents import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent -import com.tangem.feature.swap.converters.TokenMarketInfoToParamsConverter import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.TransactionFeeResult import com.tangem.feature.swap.domain.TxFeeSealedState @@ -88,7 +92,6 @@ import com.tangem.feature.swap.models.UiActions import com.tangem.feature.swap.models.market.SwapMarketsListBatchFlowManager import com.tangem.feature.swap.models.market.state.SwapMarketState import com.tangem.feature.swap.models.states.SwapNotificationUM -import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder @@ -105,11 +108,8 @@ import com.tangem.utils.Provider import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode @@ -120,6 +120,7 @@ import javax.inject.Inject typealias SuccessLoadedSwapData = Map +@OptIn(ExperimentalCoroutinesApi::class) @Suppress("LongParameterList", "LargeClass") @Stable @ModelScoped @@ -154,9 +155,8 @@ internal class SwapModel @Inject constructor( private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, private val sendFeatureToggles: SendFeatureToggles, private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, - swapFeatureToggles: SwapFeatureToggles, + private val swapFeatureToggles: SwapFeatureToggles, private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, - private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase, private val excludedBlockchains: ExcludedBlockchains, private val getUserWalletsUseCase: GetWalletsUseCase, private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase, @@ -259,10 +259,26 @@ internal class SwapModel @Inject constructor( private val searchQueryState = MutableStateFlow("") private val visibleMarketItemIds = MutableStateFlow>(emptyList()) + private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) + private var latestMarketsState: SwapMarketState? = null + + private val defaultMarketsListManager by lazy { + SwapMarketsListBatchFlowManager( + getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, + order = TokenMarketListConfig.Order.Trending, + currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, + currentSearchText = Provider { null }, + modelScope = modelScope, + dispatchers = dispatchers, + ) + } + private val searchMarketsListManager by lazy { SwapMarketsListBatchFlowManager( getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, + order = TokenMarketListConfig.Order.ByRating, currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, currentSearchText = Provider { searchQueryState.value }, modelScope = modelScope, @@ -280,13 +296,17 @@ internal class SwapModel @Inject constructor( override fun onSuccess(addedToken: CryptoCurrency) { modelScope.launch { bottomSheetNavigation.dismiss() - uiState.selectTokenState?.let { currentSelectState -> - uiState = uiState.copy( - selectTokenState = currentSelectState.copy( - marketsState = null, - ), - ) - } + analyticsEventHandler.send( + SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = addedToken.symbol), + ) + analyticsEventHandler.send( + SwapAnalyticsEvent.TokenSelected( + token = addedToken.symbol, + source = ScreensSources.Markets, + isSearched = searchQueryState.value.isNotEmpty(), + ), + ) + searchQueryState.value = "" getAccountCurrencyStatusUseCase.invoke(userWalletId, addedToken) .firstOrNull { it.status.value is CryptoCurrencyStatus.Loaded @@ -322,7 +342,7 @@ internal class SwapModel @Inject constructor( } if (fromAccountStatus == null) { - uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back) + uiState = stateBuilder.addDefaultAlert(uiState = uiState, onDismiss = swapRouter::back) } else { fromAccountCurrencyStatus = fromAccountStatus toAccountCurrencyStatus = toAccountStatus @@ -340,7 +360,7 @@ internal class SwapModel @Inject constructor( } if (fromStatus == null) { - uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back) + uiState = stateBuilder.addDefaultAlert(uiState = uiState, onDismiss = swapRouter::back) } else { initialFromStatus = fromStatus initialToStatus = toStatus @@ -363,50 +383,7 @@ internal class SwapModel @Inject constructor( } .launchIn(modelScope) - if (swapFeatureToggles.isMarketListFeatureEnabled) { - combine( - flow = searchQueryState - .onEach { searchQuery -> - searchMarketsListManager.reload(searchQuery) - }, - flow2 = searchMarketsListManager.uiItems, - flow3 = searchMarketsListManager.isInInitialLoadingErrorState, - flow4 = searchMarketsListManager.isSearchNotFoundState, - flow5 = searchMarketsListManager.totalCount.filterNotNull(), - ) { searchQuery, uiItems, isError, isSearchNotFound, total -> - when { - searchQuery.isEmpty() -> { - visibleMarketItemIds.value = emptyList() - null - } - isError -> SwapMarketState.LoadingError( - onRetryClicked = { searchMarketsListManager.reload(searchQuery) }, - ) - isSearchNotFound -> SwapMarketState.SearchNothingFound - uiItems.isEmpty() -> SwapMarketState.Loading - else -> SwapMarketState.Content( - items = uiItems, - loadMore = { searchMarketsListManager.loadMore() }, - onItemClick = { item -> - addToPortfolioItem(item) - }, - visibleIdsChanged = { visibleMarketItemIds.value = it }, - total = total, - ) - } - } - .distinctUntilChanged() - .onEach { marketsState -> - uiState.selectTokenState?.let { currentSelectState -> - uiState = uiState.copy( - selectTokenState = currentSelectState.copy( - marketsState = marketsState, - ), - ) - } - } - .launchIn(modelScope) - } + subscribeMarketTokens() modelScope.launch { visibleMarketItemIds.mapNotNull { rawIDS -> @@ -419,6 +396,18 @@ internal class SwapModel @Inject constructor( searchMarketsListManager.loadCharts(visibleBatchKeys) } } + + modelScope.launch { + visibleDefaultMarketItemIds.mapNotNull { rawIds -> + if (rawIds.isNotEmpty()) { + defaultMarketsListManager.getBatchKeysByItemIds(rawIds) + } else { + null + } + }.distinctUntilChanged().collectLatest { visibleBatchKeys -> + defaultMarketsListManager.loadCharts(visibleBatchKeys) + } + } } fun onStart() { @@ -444,6 +433,41 @@ internal class SwapModel @Inject constructor( analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(hasAvailableTokens = isAnyAvailableTokens)) } + private fun subscribeMarketTokens() { + if (swapFeatureToggles.isMarketListFeatureEnabled) { + // Switch between default and search market flows + searchQueryState + .map { it.isEmpty() } + .distinctUntilChanged() + .flatMapLatest { isDefaultMode -> + if (isDefaultMode) { + visibleMarketItemIds.value = emptyList() + createDefaultMarketsFlow() + } else { + visibleDefaultMarketItemIds.value = emptyList() + createSearchMarketsFlow() + } + } + .onEach { marketsState -> + latestMarketsState = marketsState + applyMarketsState(marketsState) + } + .launchIn(modelScope) + + // Reload search markets when query changes + searchQueryState + .onEach { searchQuery -> + if (searchQuery.isNotEmpty()) { + searchMarketsListManager.reload(searchQuery) + } + } + .launchIn(modelScope) + + // Initial load of default markets + defaultMarketsListManager.reload() + } + } + @Suppress("LongMethod") private fun initTokens(isReverseFromTo: Boolean) { modelScope.launch(dispatchers.main) { @@ -486,14 +510,7 @@ internal class SwapModel @Inject constructor( dataState.fromCryptoCurrency } - fromCryptoCurrency?.let { cryptoCurrency -> - dataState = dataState.copy( - feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = userWalletId, - cryptoCurrencyStatus = cryptoCurrency, - ).getOrNull(), - ) - } + if (fromCryptoCurrency != null) updateFeePaidCryptoCurrencyFor(fromCryptoCurrency) subscribeToCoinBalanceUpdatesIfNeeded() }.onFailure { error -> @@ -614,6 +631,18 @@ internal class SwapModel @Inject constructor( toAccount = toAccount, tokensDataState = state, ) + + if (handleSwapNotSupported( + state = state, + fromToken = fromCurrencyStatus, + toToken = toCurrencyStatus, + fromAccount = fromAccount, + toAccount = toAccount, + ) + ) { + return + } + startLoadingQuotes( fromToken = fromCurrencyStatus, fromAccount = fromAccount, @@ -641,6 +670,17 @@ internal class SwapModel @Inject constructor( fromToken = dataState.fromCryptoCurrency?.currency ?: initialCurrencyFrom, ) } + latestMarketsState?.let(::applyMarketsState) + } + + private fun applyMarketsState(marketsState: SwapMarketState) { + uiState.selectTokenState?.let { currentSelectState -> + uiState = uiState.copy( + selectTokenState = currentSelectState.copy( + marketsState = marketsState, + ), + ) + } } private fun startLoadingQuotes( @@ -700,6 +740,15 @@ internal class SwapModel @Inject constructor( } } + private suspend fun updateFeePaidCryptoCurrencyFor(fromToken: CryptoCurrencyStatus) { + dataState = dataState.copy( + feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = fromToken, + ).getOrNull(), + ) + } + private fun loadQuotesTask( fromToken: CryptoCurrencyStatus, fromAccount: Account.CryptoPortfolio?, @@ -971,7 +1020,7 @@ internal class SwapModel @Inject constructor( val fee = getSelectedFee() if (fee == null && tangemPayInput?.isWithdrawal != true) { - makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + makeSupportAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) modelScope.launch { delay(SWAP_IN_PROGRESS_DELAY) startLoadingQuotesFromLastState() @@ -997,7 +1046,7 @@ internal class SwapModel @Inject constructor( when (swapTransactionState) { is SwapTransactionState.TxSent -> { if (fee == null) { - makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + makeSupportAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) return@onSuccess } sendSuccessSwapEvent( @@ -1075,6 +1124,7 @@ internal class SwapModel @Inject constructor( cryptoAmount = swapTransactionState.cryptoAmount, cryptoCurrencyId = swapTransactionState.cryptoCurrencyId, receiverCexAddress = swapTransactionState.cexAddress, + exchangeData = swapTransactionState.exchangeData, ) .onLeft { startLoadingQuotesFromLastState() @@ -1155,7 +1205,7 @@ internal class SwapModel @Inject constructor( } val feeForPermission = when (val fee = approveDataModel.fee) { TxFeeState.Empty -> { - makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + makeSupportAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) Timber.e("Fee should not be Empty") return@launch } @@ -1276,7 +1326,17 @@ internal class SwapModel @Inject constructor( val (foundToken, foundAccount) = getSelectedTokenAndAccount(tokens, id) foundToken?.currency?.symbol?.let { symbol -> - analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = symbol)) + analyticsEventHandler.send( + SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = symbol), + ) + + analyticsEventHandler.send( + SwapAnalyticsEvent.TokenSelected( + token = symbol, + source = ScreensSources.Portfolio, + isSearched = searchQueryState.value.isNotEmpty(), + ), + ) } if (foundToken != null) { @@ -1337,6 +1397,20 @@ internal class SwapModel @Inject constructor( toAccount = toAccount, selectedProvider = null, ) + swapRouter.openScreen(SwapNavScreen.Main) + if (handleSwapNotSupported( + state = tokens, + fromToken = fromToken, + toToken = toToken, + fromAccount = fromAccount, + toAccount = toAccount, + ) + ) { + return + } + modelScope.launch { + updateFeePaidCryptoCurrencyFor(fromToken) + } startLoadingQuotes( fromToken = fromToken, fromAccount = fromAccount, @@ -1346,7 +1420,6 @@ internal class SwapModel @Inject constructor( reduceBalanceBy = lastReducedBalanceBy.value, toProvidersList = findSwapProviders(fromToken, toToken), ) - swapRouter.openScreen(SwapNavScreen.Main) updateTokensState(tokens) } } @@ -1479,6 +1552,7 @@ internal class SwapModel @Inject constructor( toAccount = newToAccount, ) isOrderReversed = !isOrderReversed + updateFeePaidCryptoCurrencyFor(newFromToken) dataState.tokensDataState?.let { tokensDataState -> updateTokensState(tokensDataState) } @@ -1584,11 +1658,15 @@ internal class SwapModel @Inject constructor( } private fun makeDefaultAlert() { - uiState = stateBuilder.addAlert(uiState) + uiState = stateBuilder.addDefaultAlert(uiState = uiState) } - private fun makeDefaultAlert(message: TextReference) { - uiState = stateBuilder.addAlert(uiState, message) + private fun makeSupportAlert(message: TextReference) { + uiState = stateBuilder.addSupportAlert( + uiState = uiState, + message = message, + onSupportClick = { onFailedTxEmailClick("Fee calculation error") }, + ) } @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -1872,6 +1950,59 @@ internal class SwapModel @Inject constructor( .orEmpty() } + /** + * @return true if swap is not supported and UI was updated to show error state + */ + private fun handleSwapNotSupported( + state: TokensDataStateExpress, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, + fromAccount: Account.CryptoPortfolio?, + toAccount: Account.CryptoPortfolio?, + ): Boolean { + val selectedCurrency = if (isOrderReversed) fromToken else toToken + if (isTokenAvailableForSwap(state, selectedCurrency, isOrderReversed)) return false + + analyticsEventHandler.send( + SwapEvents.NoticeUnavailableToSwapPair( + sendToken = fromToken.currency.symbol, + receiveToken = toToken.currency.symbol, + sendBlockchain = fromToken.currency.network.name, + receiveBlockchain = toToken.currency.network.name, + ), + ) + uiState = stateBuilder.createSwapNotSupportedState( + uiStateHolder = uiState, + fromToken = fromToken, + toToken = toToken, + fromAccount = fromAccount, + toAccount = toAccount, + ) + return true + } + + private fun isTokenAvailableForSwap( + state: TokensDataStateExpress, + selectedCurrency: CryptoCurrencyStatus, + isReverseFromTo: Boolean, + ): Boolean { + val group = if (isReverseFromTo) state.fromGroup else state.toGroup + val idToFind = selectedCurrency.currency.id.value + + return if (accountsFeatureToggles.isFeatureEnabled) { + group.accountCurrencyList.any { (_, currencyList) -> + currencyList.any { accountSwapCurrency -> + idToFind == accountSwapCurrency.cryptoCurrencyStatus.currency.id.value && + accountSwapCurrency.isAvailable + } + } + } else { + group.available.any { swapAvailability -> + idToFind == swapAvailability.currencyStatus.currency.id.value + } + } + } + private fun List.filterForTangemPayWithdrawal(): List { return if (tangemPayInput?.isWithdrawal == true) { filter { it.type == ExchangeProviderType.CEX } @@ -2059,32 +2190,98 @@ internal class SwapModel @Inject constructor( } } + private fun createDefaultMarketsFlow(): Flow { + val marketsTitle = TextReference.Res(R.string.feed_trending_now) + return combine( + defaultMarketsListManager.uiItems, + defaultMarketsListManager.isInInitialLoadingErrorState, + defaultMarketsListManager.totalCount, + ) { uiItems, isError, total -> + when { + isError -> SwapMarketState.LoadingError( + onRetryClicked = { defaultMarketsListManager.reload() }, + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + uiItems.isEmpty() -> SwapMarketState.Loading( + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + else -> SwapMarketState.Content( + items = uiItems, + loadMore = { defaultMarketsListManager.loadMore() }, + onItemClick = { item -> addToPortfolioItem(item) }, + visibleIdsChanged = { visibleDefaultMarketItemIds.value = it }, + total = total ?: uiItems.size, + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + } + } + } + + private fun createSearchMarketsFlow(): Flow { + val marketsTitle = TextReference.Res(R.string.markets_common_title) + return combine( + flow = searchMarketsListManager.uiItems, + flow2 = searchMarketsListManager.isInInitialLoadingErrorState, + flow3 = searchMarketsListManager.isSearchNotFoundState, + flow4 = searchMarketsListManager.totalCount, + ) { uiItems, isError, isSearchNotFound, total -> + when { + isError -> SwapMarketState.LoadingError( + onRetryClicked = { searchMarketsListManager.reload(searchQueryState.value) }, + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) + isSearchNotFound -> SwapMarketState.SearchNothingFound + uiItems.isEmpty() -> SwapMarketState.Loading( + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) + else -> SwapMarketState.Content( + items = uiItems, + loadMore = { searchMarketsListManager.loadMore() }, + onItemClick = { item -> addToPortfolioItem(item) }, + visibleIdsChanged = { visibleMarketItemIds.value = it }, + total = total ?: uiItems.size, + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) + } + } + } + private fun addToPortfolioItem(item: MarketsListItemUM) { modelScope.launch { - val tokenInfo = getTokenMarketInfoUseCase( - selectedAppCurrencyFlow.value, - item.id, - item.currencySymbol, - ).getOrNull() ?: return@launch + val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id) + ?: searchMarketsListManager.getTokenMarketById(item.id) + ?: return@launch - val converter = TokenMarketInfoToParamsConverter() - val param = converter.convert(tokenInfo) + val param = tokenMarket.toSerializableParam() val hasOnlyHotWallets = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot } - val networks = tokenInfo.networks?.filter { network -> + val networks = tokenMarket.networks?.filter { network -> BlockchainUtils.isSupportedNetworkId( blockchainId = network.networkId, excludedBlockchains = excludedBlockchains, hotExcludedBlockchains = hotWalletExcludedBlockchains, hasOnlyHotWallets = hasOnlyHotWallets, ) + }?.map { network -> + TokenMarketInfo.Network( + networkId = network.networkId, + isExchangeable = false, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) }.orEmpty() addToPortfolioManager = addToPortfolioManagerFactory .create( scope = modelScope, token = param, - analyticsParams = null, + analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value), ).apply { setTokenNetworks(networks) } @@ -2166,6 +2363,8 @@ internal class SwapModel @Inject constructor( FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true), ) + override val forceUpdateState = MutableSharedFlow() + override suspend fun loadFeeExtended( selectedToken: CryptoCurrencyStatus?, ): Either { @@ -2198,18 +2397,15 @@ internal class SwapModel @Inject constructor( } override fun onResult(newState: FeeSelectorUM) { - if (isPermissionNotificationShown()) { - state.value = FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true) - return - } + state.value = newState if (newState is FeeSelectorUM.Error) { - state.value = newState.copy(isHidden = true) + modelScope.launch { + forceUpdateState.emit(newState.copy(isHidden = true)) + } return } - state.value = newState - // If fee currency is same as from currency, we need to reload quotes to update fee info val isFeeCurrencySameAsFromCurrency = newState is FeeSelectorUM.Content && dataState.fromCryptoCurrency?.currency?.id == newState.feeExtraInfo.feeCryptoCurrencyStatus.currency.id diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 6c9bd0a484..d57d2943f2 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -64,6 +64,12 @@ internal class SwapNotificationsFactory( ) } + fun getSwapNotSupportedNotifications(): ImmutableList { + return persistentListOf( + SwapNotificationUM.Warning.SwapNotSupported, + ) + } + fun getQuotesErrorStateNotifications( expressDataError: ExpressDataError, fromToken: CryptoCurrency, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt index bb84996732..bb7763f928 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt @@ -7,7 +7,16 @@ import com.tangem.core.ui.extensions.resourceReference sealed class SwapAlertUM : AlertUM { - data class GenericError( + data class DefaultError( + override val onConfirmClick: (() -> Unit), + override val message: TextReference = resourceReference(R.string.common_unknown_error), + ) : SwapAlertUM() { + override val title: TextReference? = null + override val confirmButtonText: TextReference = + resourceReference(id = R.string.common_ok) + } + + data class SupportError( override val onConfirmClick: (() -> Unit), override val message: TextReference = resourceReference(R.string.common_unknown_error), ) : SwapAlertUM() { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt index 3e7b9af39f..77609fade7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt @@ -22,12 +22,14 @@ import kotlinx.coroutines.flow.* internal class SwapMarketsListBatchFlowManager( getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType, + private val order: TokenMarketListConfig.Order, private val currentAppCurrency: Provider, private val currentSearchText: Provider, private val modelScope: CoroutineScope, private val dispatchers: CoroutineDispatcherProvider, ) { - private val actionsFlow = MutableSharedFlow>() + private val actionsFlow = + MutableSharedFlow>(replay = 1) private val updateStateJob = JobHolder() private val batchFlow = getMarketsTokenListFlowUseCase( @@ -183,7 +185,8 @@ internal class SwapMarketsListBatchFlowManager( searchText ?: currentSearchText() }, priceChangeInterval = TokenMarketListConfig.Interval.H24, - order = TokenMarketListConfig.Order.ByRating, + order = order, + shouldNetworks = true, ), ), ) @@ -236,6 +239,13 @@ internal class SwapMarketsListBatchFlowManager( .toSet() } + fun getTokenMarketById(id: CryptoCurrency.RawID): TokenMarket? { + return batchFlow.state.value.data + .asSequence() + .flatMap { it.data } + .firstOrNull { it.id == id } + } + private data class ResultBatches( val uiBatches: List>> = emptyList(), val processedItems: List>>? = null, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/converter/SwapMarketsTokenItemConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/converter/SwapMarketsTokenItemConverter.kt index 78c52b2aab..31ea67416e 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/converter/SwapMarketsTokenItemConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/converter/SwapMarketsTokenItemConverter.kt @@ -44,6 +44,13 @@ internal class SwapMarketsTokenItemConverter( resourceReference(R.string.markets_apy_placeholder, wrappedList(it)) }, updateTimestamp = value.updateTimestamp, + networks = value.networks?.map { network -> + MarketsListItemUM.Network( + networkId = network.networkId, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) + }, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt index 50e6d14a74..8be842bcfd 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt @@ -2,25 +2,40 @@ package com.tangem.feature.swap.models.market.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.core.ui.R import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class SwapMarketState { + abstract val marketsTitle: TextReference + abstract val shouldAssetsCount: Boolean + data class Content( val items: ImmutableList, val total: Int, val loadMore: () -> Unit, val onItemClick: (MarketsListItemUM) -> Unit, val visibleIdsChanged: (List) -> Unit, + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, ) : SwapMarketState() - data object Loading : SwapMarketState() + data class Loading( + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, + ) : SwapMarketState() data class LoadingError( val onRetryClicked: () -> Unit, + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, ) : SwapMarketState() - data object SearchNothingFound : SwapMarketState() + data object SearchNothingFound : SwapMarketState() { + override val marketsTitle: TextReference = TextReference.Res(R.string.markets_common_title) + override val shouldAssetsCount: Boolean = true + } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt index d140ef1ff0..e158fd5f3b 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt @@ -132,6 +132,15 @@ internal object SwapNotificationUM { ), ) + data object SwapNotSupported : Warning( + title = resourceReference( + id = com.tangem.feature.swap.presentation.R.string.warning_express_unsupported_pair_title, + ), + subtitle = resourceReference( + com.tangem.feature.swap.presentation.R.string.warning_express_unsupported_pair_description, + ), + ) + data class NeedReserveToCreateAccount( val receiveAmount: String, val receiveToken: String, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index c3a85fe555..6e0f5f520a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -170,10 +170,10 @@ internal class StateBuilder( amountTextFieldValue = null, amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", token = fromToken, - tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, - coinId = uiStateHolder.sendCardData.coinId, - isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, - tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, + tokenIconUrl = fromToken.currency.iconUrl, + coinId = fromToken.currency.network.backendId, + isNotNativeToken = fromToken.currency is CryptoCurrency.Token, + tokenCurrency = fromToken.currency.symbol, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, balance = fromToken.getFormattedAmount(isNeedSymbol = false), networkIconRes = getActiveIconRes(fromToken.currency.network.rawId), @@ -200,6 +200,63 @@ internal class StateBuilder( ) } + fun createSwapNotSupportedState( + uiStateHolder: SwapStateHolder, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, + fromAccount: Account.CryptoPortfolio?, + toAccount: Account.CryptoPortfolio?, + ): SwapStateHolder { + if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder + return uiStateHolder.copy( + sendCardData = SwapCardState.SwapCardData( + type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable).copy( + accountTitleUM = getFromCardAccountTitle(fromAccount), + ), + amountTextFieldValue = null, + amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + token = fromToken, + tokenIconUrl = fromToken.currency.iconUrl, + coinId = fromToken.currency.network.backendId, + isNotNativeToken = fromToken.currency is CryptoCurrency.Token, + tokenCurrency = fromToken.currency.symbol, + canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, + balance = fromToken.getFormattedAmount(isNeedSymbol = false), + networkIconRes = getActiveIconRes(fromToken.currency.network.rawId), + isBalanceHidden = isBalanceHiddenProvider(), + ), + receiveCardData = SwapCardState.SwapCardData( + type = TransactionCardType.ReadOnly( + accountTitleUM = getToCardAccountTitle(toAccount), + ), + amountTextFieldValue = TextFieldValue( + text = "0", + ), + amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + token = toToken, + tokenIconUrl = toToken.currency.iconUrl, + coinId = toToken.currency.network.backendId, + isNotNativeToken = toToken.currency is CryptoCurrency.Token, + tokenCurrency = toToken.currency.symbol, + canSelectAnotherToken = true, + balance = toToken.getFormattedAmount(isNeedSymbol = false), + networkIconRes = getActiveIconRes(toToken.currency.network.rawId), + isBalanceHidden = isBalanceHiddenProvider(), + ), + notifications = notificationsFactory.getSwapNotSupportedNotifications(), + fee = FeeItemState.Empty, + swapButton = SwapButton( + walletInteractionIcon = walletInterationIcon(userWalletProvider()), + isEnabled = false, + isHoldToConfirm = isHoldToConfirmEnabled, + onClick = { }, + ), + changeCardsButtonState = ChangeCardsButtonState.DISABLED, + providerState = ProviderState.Empty(), + priceImpact = PriceImpact.Empty(), + ) + } + @Suppress("LongParameterList") fun createQuotesLoadingState( uiStateHolder: SwapStateHolder, @@ -438,6 +495,7 @@ internal class StateBuilder( notification is SwapNotificationUM.Warning.ExpressError || notification is SwapNotificationUM.Warning.ExpressGeneralError || notification is SwapNotificationUM.Warning.NoAvailableTokensToSwap || + notification is SwapNotificationUM.Warning.SwapNotSupported || notification is SwapNotificationUM.Warning.NeedReserveToCreateAccount || notification is SwapNotificationUM.Info.PermissionNeeded } @@ -1011,7 +1069,7 @@ internal class StateBuilder( ) } - fun addAlert( + fun addDefaultAlert( uiState: SwapStateHolder, message: TextReference = resourceReference(R.string.common_unknown_error), onDismiss: () -> Unit = { clearAlert(uiState) }, @@ -1019,7 +1077,23 @@ internal class StateBuilder( return uiState.copy( event = triggeredEvent( SwapEvent.ShowAlert( - SwapAlertUM.GenericError(onDismiss, message), + SwapAlertUM.DefaultError(onDismiss, message), + ), + onConsume = onDismiss, + ), + ) + } + + fun addSupportAlert( + uiState: SwapStateHolder, + message: TextReference = resourceReference(R.string.common_unknown_error), + onDismiss: () -> Unit = { clearAlert(uiState) }, + onSupportClick: () -> Unit, + ): SwapStateHolder { + return uiState.copy( + event = triggeredEvent( + SwapEvent.ShowAlert( + SwapAlertUM.SupportError(onSupportClick, message), ), onConsume = onDismiss, ), 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/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index e55c8398bf..b4d3c15097 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -383,10 +383,7 @@ private fun MainButton(state: SwapStateHolder) { state.swapButton.isHoldToConfirm -> { HoldToConfirmButton( modifier = Modifier.fillMaxWidth(), - text = stringResourceSafe( - R.string.common_hold_to, - stringResourceSafe(id = R.string.swapping_swap_action), - ), + text = stringResourceSafe(R.string.swapping_swap_action), enabled = state.swapButton.isEnabled, onConfirm = state.swapButton.onClick, isLoading = state.swapButton.isInProgress, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 2bc3cc5c61..2a21dd13ec 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -189,7 +189,7 @@ private fun ListOfTokensWithMarkets( state = lazyListState, ) { if (state.tokensListData !is TokenListUMData.EmptyList) { - assetsTitle(count = state.tokensListData.totalTokensCount) + assetsTitle(count = state.tokensListData.totalTokensCount, showCount = marketsState.shouldAssetsCount) } tokensListItems( @@ -197,10 +197,18 @@ private fun ListOfTokensWithMarkets( isBalanceHidden = state.isBalanceHidden, ) - if (state.tokensListData is TokenListUMData.EmptyList) { + tokensToSelectItems(state.availableTokens, state.onTokenSelected) + if (state.unavailableTokens.isNotEmpty()) { item { SpacerH12() } - } else { + tokensToSelectItems(state.unavailableTokens, state.onTokenSelected) + } + + val hasPortfolioContent = state.tokensListData !is TokenListUMData.EmptyList || + state.availableTokens.isNotEmpty() + if (hasPortfolioContent) { item { SpacerH32() } + } else { + item { SpacerH12() } } swapMarketsListItems(marketsState) @@ -242,13 +250,15 @@ private fun VisibleItemsTracker(lazyListState: LazyListState, marketState: SwapM } } -private fun LazyListScope.assetsTitle(count: Int) { +private fun LazyListScope.assetsTitle(count: Int, showCount: Boolean) { item(key = "assets_title") { Text( text = buildAnnotatedString { append(stringResourceSafe(R.string.swap_your_assets_title)) - withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { - append(" $count") + if (showCount) { + withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { + append(" $count") + } } }, style = TangemTheme.typography.h3, @@ -312,7 +322,7 @@ internal fun LazyListScope.portfolioTokensList(portfolio: TokensListItemUM.Portf portfolioItem( portfolio = portfolio, - modifier = Modifier.padding(top = 8.dp), + modifier = Modifier, isBalanceHidden = isBalanceHidden, ) if (!isExpanded) return diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 373d309f0f..25aac0f046 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -67,13 +67,21 @@ fun TransactionCard( @DrawableRes networkIconRes: Int? = null, onChangeTokenClick: (() -> Unit)? = null, ) { + val cardTag = when (type) { + is TransactionCardType.Inputtable -> + SwapTokenScreenTestTags.SWAP_CARD + is TransactionCardType.ReadOnly -> + SwapTokenScreenTestTags.RECEIVE_CARD + } + Box( modifier = modifier .background( shape = RoundedCornerShape(TangemTheme.dimens.radius16), color = TangemTheme.colors.background.primary, ) - .fillMaxSize(), + .fillMaxSize() + .testTag(cardTag), ) { Column( modifier = Modifier @@ -327,6 +335,7 @@ private fun Content( text = amount, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, + modifier = Modifier.testTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT), ) } } @@ -357,7 +366,9 @@ private fun Content( text = amount, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, - modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20), + modifier = Modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size20) + .testTag(SwapTokenScreenTestTags.SWAP_FIAT_AMOUNT), ) } } @@ -504,7 +515,9 @@ fun ChangeTokenSelector() { contentAlignment = Alignment.CenterEnd, ) { Icon( - modifier = Modifier.size(TangemTheme.dimens.size20), + modifier = Modifier + .size(TangemTheme.dimens.size20) + .testTag(SwapTokenScreenTestTags.SELECT_TOKEN_ICON), painter = painterResource(id = R.drawable.ic_chevron_24), tint = TangemTheme.colors.icon.secondary, contentDescription = null, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt index 0f69e31f8b..ced4fbda3e 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt @@ -14,6 +14,7 @@ import com.tangem.common.ui.markets.MarketsListItem import com.tangem.common.ui.markets.MarketsListItemPlaceholder import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.market.state.SwapMarketState @@ -24,7 +25,7 @@ internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) { val totalCount = (state as? SwapMarketState.Content)?.total Text( text = buildAnnotatedString { - append(stringResourceSafe(R.string.markets_common_title)) + append(state.marketsTitle.resolveReference()) if (totalCount != null) { withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { append(" $totalCount") diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt index b9a6ab2d40..8c03c69176 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt @@ -4,8 +4,10 @@ import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.core.ui.R import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenListUMData @@ -35,6 +37,8 @@ internal class SwapSelectTokenPreviewProvider { onItemClick = { }, visibleIdsChanged = { }, total = TOTAL_ITEMS, + marketsTitle = TextReference.Res(R.string.feed_trending_now), + shouldAssetsCount = false, ) private fun createPreviewMarketItems() = listOf( 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/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index 8dfb45c420..beb2bed482 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -26,8 +26,8 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayTopUpData -import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.domain.pay.repository.TangemPayWithdrawRepository import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.domain.visa.model.TangemPayTxHistoryItem @@ -79,7 +79,7 @@ internal class TangemPayDetailsModel @Inject constructor( private val cardDetailsEventListener: CardDetailsEventListener, private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener, private val tangemPayCryptoCurrencyFactory: TangemPayCryptoCurrencyFactory, - private val orderRepository: CustomerOrderRepository, + private val tangemPayWithdrawRepository: TangemPayWithdrawRepository, private val getUserWalletUseCase: GetUserWalletUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val expressTransactionsEventListener: ExpressTransactionsEventListener, @@ -124,6 +124,7 @@ internal class TangemPayDetailsModel @Inject constructor( modelScope.launch { expressTransactionsEventListener.send(ExpressTransactionsEvent.Update) } + subscribeToWithdrawOrder() } fun onPause() { @@ -148,6 +149,14 @@ internal class TangemPayDetailsModel @Inject constructor( .launchIn(modelScope) } + private fun subscribeToWithdrawOrder() { + modelScope.launch { + val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull() + ?: return@launch + tangemPayWithdrawRepository.pollWithdrawOrdersIfNeeds(userWallet) + } + } + override fun onClickPinCode() { analytics.send(TangemPayAnalyticsEvents.PinCodeClicked()) if (!params.config.isPinSet) { @@ -278,24 +287,28 @@ internal class TangemPayDetailsModel @Inject constructor( if (currentBalance == null || depositAddress == null) { showBottomSheetError(TangemPayDetailsErrorType.Withdraw) } else { - modelScope.launch { - val hasActiveWithdrawal = orderRepository.hasWithdrawOrder(userWalletId = params.userWalletId) - if (hasActiveWithdrawal) { - showBottomSheetError(TangemPayDetailsErrorType.WithdrawInProgress) - } else { - val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull() - val currency = cryptoCurrency ?: userWallet?.let { - tangemPayCryptoCurrencyFactory.create(userWallet = userWallet, chainId = params.config.chainId) - .getOrNull() - } - if (currency != null) { - uiMessageSender.send( - message = TangemPayMessagesFactory.createWithdrawWarning( - onGotItClick = { onConfirmWithdrawal(currency, currentBalance, depositAddress) }, - ), - ) + val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull() + if (userWallet == null) { + showBottomSheetError(TangemPayDetailsErrorType.Withdraw) + } else { + modelScope.launch { + val hasActiveWithdrawal = tangemPayWithdrawRepository.hasWithdrawOrder(userWallet = userWallet) + if (hasActiveWithdrawal) { + showBottomSheetError(TangemPayDetailsErrorType.WithdrawInProgress) } else { - showBottomSheetError(TangemPayDetailsErrorType.Withdraw) + val currency = cryptoCurrency ?: tangemPayCryptoCurrencyFactory.create( + userWallet = userWallet, + chainId = params.config.chainId, + ).getOrNull() + if (currency != null) { + uiMessageSender.send( + message = TangemPayMessagesFactory.createWithdrawWarning( + onGotItClick = { onConfirmWithdrawal(currency, currentBalance, depositAddress) }, + ), + ) + } else { + showBottomSheetError(TangemPayDetailsErrorType.Withdraw) + } } } } @@ -437,7 +450,7 @@ internal class TangemPayDetailsModel @Inject constructor( analytics.send(TangemPayAnalyticsEvents.ReceiveFundsClicked()) bottomSheetNavigation.dismiss() val config = TokenReceiveConfig( - shouldShowWarning = false, + shouldShowWarning = true, cryptoCurrency = data.currency, userWalletId = data.walletId, showMemoDisclaimer = false, 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/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt index 0d855a3880..2bc39f10d0 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt @@ -212,7 +212,7 @@ internal class TesterAccountsViewModel @Inject constructor( name = "Account #$nextIndex", icon = CryptoPortfolioIcon.ofDefaultCustomAccount(), derivationIndex = nextIndex, - cryptoCurrencies = emptySet(), + cryptoCurrencies = emptyList(), ) .getOrNull() ?: break diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt index 3dd7bc3947..70f0fe63d0 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt @@ -52,9 +52,7 @@ internal class DefaultTokenReceiveComponent @AssistedInject constructor( ) } - override fun dismiss() { - model.params.onDismiss() - } + override fun dismiss() = model.params.onDismiss() private fun onChildBack() { when (contentStack.value.active.configuration) { 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..950bd50f41 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt @@ -0,0 +1,58 @@ +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() + return state.copy(dialogSlot = null) + } + + fun getStateWithClosedBottomSheet(): ExpressTransactionsBlockState { + val state = currentStateProvider() + return state.copy(bottomSheetSlot = null) + } + + fun getStateWithConfirmHideExpressStatus(): ExpressTransactionsBlockState { + return currentStateProvider().copy( + dialogSlot = TokenDetailsDialogConfig( + isShow = true, + onDismissRequest = expressTransactionsClickIntents::onDismissDialog, + content = TokenDetailsDialogConfig.DialogContentConfig.ConfirmExpressStatusHideDialogConfig( + onConfirmClick = { + expressTransactionsClickIntents.onDisposeExpressStatus() + expressTransactionsClickIntents.onDismissDialog() + }, + onCancelClick = expressTransactionsClickIntents::onDismissDialog, + ), + ).toDialogSlot(), + ) + } + + private fun TokenDetailsDialogConfig.toDialogSlot(): DialogSlot { + val contentLambda: @Composable () -> Unit = { + TokenDetailsDialogs(this) + } + return DialogSlot(config = this, content = contentLambda) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/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/AccountItemsDelegate.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt index aa4c1a4b46..6c53807de1 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt @@ -22,6 +22,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents @@ -84,11 +85,11 @@ internal class AccountItemsDelegate @Inject constructor( return WalletSettingsAccountsUM.Account(state = accountItemUM) } - fun mapAccount(account: AccountStatus): WalletSettingsAccountsUM = when (account) { + fun mapAccount(account: AccountStatus.CryptoPortfolio): WalletSettingsAccountsUM = when (account) { is AccountStatus.CryptoPortfolio -> account.mapCryptoPortfolio() } - val accounts = accountStatusList.accountStatuses + val accounts = accountStatusList.accountStatuses.filterCryptoPortfolio() val header = WalletSettingsAccountsUM.Header( id = "accounts_header", diff --git a/features/wallet-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..256d99fb51 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -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 } @@ -495,6 +487,7 @@ internal class WalletModel @Inject constructor( is WalletsUpdateActionResolver.Action.ReloadWallets -> { reloadWarnings(action) } + is WalletsUpdateActionResolver.Action.ReorderWallets -> reorderWallets(action) WalletsUpdateActionResolver.Action.EmptyWallets -> { Timber.w("Wallets list is empty!") } @@ -504,6 +497,25 @@ internal class WalletModel @Inject constructor( } } + private fun reorderWallets(action: WalletsUpdateActionResolver.Action.ReorderWallets) { + val currentWalletId = stateHolder.getSelectedWalletId() + val currentIndex = stateHolder.getWalletIndexByWalletId(userWalletId = currentWalletId) + + stateHolder.update( + transformer = ReorderWalletsTransformer( + wallets = action.wallets, + ), + ) + + val newIndex = stateHolder.getWalletIndexByWalletId(userWalletId = currentWalletId) + + if (currentIndex != null && newIndex != null && currentIndex != newIndex) { + scrollToWallet(prevIndex = currentIndex, newIndex = newIndex) { + stateHolder.update { it.copy(selectedWalletIndex = newIndex) } + } + } + } + private fun reloadWarnings(action: WalletsUpdateActionResolver.Action.ReloadWallets) { action.wallets.forEach { walletScreenContentLoader.load( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt index 4946ef99e9..1f63082dad 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt @@ -82,6 +82,9 @@ internal class WalletsUpdateActionResolver @Inject constructor( isWalletsCountChanged(state, wallets) -> { getChangeWalletsListAction(state, wallets, selectedWallet) } + isWalletsOrderChanged(state, wallets) -> { + Action.ReorderWallets(wallets = wallets) + } isAnotherWalletSelected(state, selectedWallet) -> { Action.ReinitializeNewWallet( prevWalletId = state.getPrevSelectedWallet().id, @@ -121,6 +124,13 @@ internal class WalletsUpdateActionResolver @Inject constructor( return prevWalletsSize != walletsSize } + private fun isWalletsOrderChanged(state: WalletScreenState, wallets: List): Boolean { + val prevWalletIds = state.wallets.map { it.walletCardState.id } + val newWalletIds = wallets.map { it.walletId } + + return prevWalletIds != newWalletIds + } + private fun getChangeWalletsListAction( state: WalletScreenState, wallets: List, @@ -394,6 +404,15 @@ internal class WalletsUpdateActionResolver @Inject constructor( val wallets: List, ) : Action() + data class ReorderWallets( + val wallets: List, + ) : Action() { + + override fun toString(): String { + return "ReorderWallets(wallets = ${wallets.joinToString { it.walletId.toString() }})" + } + } + data object EmptyWallets : Action() data object Unknown : Action() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/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..0cdab04aa3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -11,12 +11,13 @@ 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 import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.hotwallet.CloseHotWalletUpgradeBannerUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -37,22 +38,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 +68,6 @@ internal interface WalletWarningsClickIntents { fun onOpenUnlockWalletsBottomSheetClick() - fun onUnlockWalletClick() - - fun onUnlockVisaAccessClick() - - fun onScanToUnlockWalletClick() - fun onLikeAppClick() fun onDislikeAppClick() @@ -93,6 +80,8 @@ internal interface WalletWarningsClickIntents { fun onSupportClick() + fun onBackupErrorClick() + fun onNoteMigrationButtonClick(url: String) fun onSeedPhraseNotificationConfirm() @@ -110,6 +99,10 @@ internal interface WalletWarningsClickIntents { fun onFinishWalletActivationClick(isBackupExists: Boolean) fun onYieldPromoTermsAndConditionsClick() + + fun onUpgradeHotWalletClick(userWalletId: UserWalletId) + + fun onCloseUpgradeBannerClick(userWalletId: UserWalletId) } @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @@ -122,8 +115,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 +128,14 @@ 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, + private val closeHotWalletUpgradeBannerUseCase: CloseHotWalletUpgradeBannerUseCase, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { @@ -205,81 +197,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 +218,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 +272,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( action = PromotionBannerClicked.BannerAction.Closed(), ) }, - ) modelScope.launch(dispatchers.main) { shouldShowPromoWalletUseCase.neverToShow(promoId) @@ -414,6 +340,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 +361,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 +379,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 +393,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), + ) + } + }, ) } @@ -630,6 +550,24 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( urlOpener.openUrl(YIELD_PROMO_TERMS_LINK) } + override fun onUpgradeHotWalletClick(userWalletId: UserWalletId) { + modelScope.launch(dispatchers.main) { + val userWallet = getUserWalletUseCase(userWalletId).getOrNull() + if (userWallet is UserWallet.Hot) { + appRouter.push(UpgradeWallet(userWalletId)) + } + } + } + + override fun onCloseUpgradeBannerClick(userWalletId: UserWalletId) { + modelScope.launch(dispatchers.main) { + val userWallet = getUserWalletUseCase(userWalletId).getOrNull() + if (userWallet is UserWallet.Hot) { + closeHotWalletUpgradeBannerUseCase(userWalletId) + } + } + } + private companion object { const val VISA_PROMO_LINK = "https://tangem.com/en/cardwaitlist/?utm_source=tangem-app-banner" + "&utm_medium=banner" + diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/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/account/ExpandedAccountsHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt index 59095ad0df..b1c48cff7e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/ExpandedAccountsHolder.kt @@ -10,7 +10,9 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -43,10 +45,13 @@ internal class ExpandedAccountsHolder @Inject constructor( .toSet() // main state holder val expandedAccounts = MutableStateFlow(initExpandedState) + var debounceJob: Job? = null actionChannel .filter { (accountId, _) -> accountId.userWalletId == walletId } + .filter { debounceJob?.isActive != true } .onEach { (accountId, isExpand) -> + debounceJob = launch { delay(DEBOUNCE_MILLIS) } val newState = AccountExpandedState(accountId, isExpand) launch { accountsExpandedRepository.update(newState) } if (isExpand) { @@ -100,4 +105,8 @@ internal class ExpandedAccountsHolder @Inject constructor( } private fun walletAccounts(walletId: UserWalletId): Flow = singleAccountListSupplier(walletId) + + companion object { + private const val DEBOUNCE_MILLIS = 200L + } } \ No newline at end of file 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/CryptoCurrenciesIdsResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt index d62385f7be..770ada3985 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt @@ -2,7 +2,7 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.model.AccountCryptoCurrencies -import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.tokenlist.TokenList import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem @@ -34,6 +34,7 @@ internal class CryptoCurrenciesIdsResolver { } } + @Suppress("UseOrEmpty") fun resolveV2(tokensListUM: OrganizeTokensListUM, accountStatusList: AccountStatusList?): AccountCryptoCurrencies { val draggableTokens = when (tokensListUM) { OrganizeTokensListUM.EmptyList -> return emptyMap() @@ -43,10 +44,11 @@ internal class CryptoCurrenciesIdsResolver { } return accountStatusList?.accountStatuses - ?.filter { it.getCryptoTokenList() != TokenList.Empty } + ?.filterCryptoPortfolio() + ?.filter { it.tokenList != TokenList.Empty } ?.associate { accountStatus -> val currencies = accountStatus.flattenCurrencies() - accountStatus.account as Account.CryptoPortfolio to draggableTokens + accountStatus.account to draggableTokens .asSequence() .filter { it.accountId == accountStatus.account.accountId.value } .mapNotNull { sortedToken -> diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt index 75e1c92282..279d59f8ed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt @@ -6,7 +6,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.tokenlist.TokenList import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM @@ -61,7 +61,7 @@ internal class AccountTokenItemConverter( isGrouped = isGrouping, items = value.accountStatuses .asSequence() - .filterIsInstance() + .filterCryptoPortfolio() .flatMap { accountStatus -> if (accountStatus.tokenList != TokenList.Empty) { buildList { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/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/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt index 295848f5f4..c0938d2748 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt @@ -2,7 +2,6 @@ package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.it import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.tokenlist.TokenList import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem @@ -13,27 +12,26 @@ import kotlinx.collections.immutable.toPersistentList internal class OrganizedTokenListConverter( private val appCurrency: AppCurrency, -) : Converter> { +) : Converter> { private val tokensConverter by lazy { CryptoCurrencyToDraggableItemConverterV2(appCurrency) } private val groupsConverter by lazy { NetworkGroupToDraggableItemsConverterV2(tokensConverter) } - override fun convert(value: AccountStatus): PersistentList { - val cryptoAccount = value.account as? Account.CryptoPortfolio ?: return persistentListOf() - return when (val tokenList = value.getCryptoTokenList()) { + override fun convert(value: AccountStatus.CryptoPortfolio): PersistentList { + return when (val tokenList = value.tokenList) { is TokenList.GroupedByNetwork -> groupsConverter.convertList( - tokenList.groups.map { cryptoAccount to it }, + tokenList.groups.map { value.account to it }, ) .flatten() .toPersistentList() is TokenList.Ungrouped -> tokensConverter.convertList( - value.flattenCurrencies().map { + value.flattenCurrencies().map { cryptoCurrencyStatus -> AccountCryptoCurrencyStatus( - account = cryptoAccount, - status = it, + account = value.account, + status = cryptoCurrencyStatus, ) }, ).toPersistentList() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index f92476350d..11aaab2b47 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -33,7 +33,7 @@ sealed class WalletScreenAnalyticsEvent { put(AnalyticsParam.BALANCE, balance.value) tokensCount?.let { put(AnalyticsParam.TOKENS_COUNT, it.toString()) } }, - ) + ), AppsFlyerIncludedEvent class TokenBalance(balance: AnalyticsParam.EmptyFull, token: String) : Basic( event = "Token Balance", diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 3852ada00e..341c2c6726 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -6,8 +6,9 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.* import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType -import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.MainScreen.* +import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.PushBannerPromo.* import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider @@ -40,16 +41,16 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( @Suppress("CyclomaticComplexMethod") private fun getEvent(warning: WalletNotification): AnalyticsEvent? { return when (warning) { - is WalletNotification.Critical.DevCard -> MainScreen.DevelopmentCard() - is WalletNotification.Critical.FailedCardValidation -> MainScreen.ProductSampleCard() - is WalletNotification.Warning.MissingBackup -> MainScreen.BackupYourWallet() - is WalletNotification.Warning.NumberOfSignedHashesIncorrect -> MainScreen.CardSignedTransactions() - is WalletNotification.Warning.TestNetCard -> MainScreen.TestnetCard() - is WalletNotification.Informational.DemoCard -> MainScreen.DemoCard() - is WalletNotification.Informational.MissingAddresses -> MainScreen.MissingAddresses() - is WalletNotification.RateApp -> MainScreen.HowDoYouLikeTangem() - is WalletNotification.Critical.BackupError -> MainScreen.BackupError() - is WalletNotification.NoteMigration -> MainScreen.NotePromo() + is WalletNotification.Critical.DevCard -> DevelopmentCard() + is WalletNotification.Critical.FailedCardValidation -> ProductSampleCard() + is WalletNotification.Warning.MissingBackup -> BackupYourWallet() + is WalletNotification.Warning.NumberOfSignedHashesIncorrect -> CardSignedTransactions() + is WalletNotification.Warning.TestNetCard -> TestnetCard() + is WalletNotification.Informational.DemoCard -> DemoCard() + is WalletNotification.Informational.MissingAddresses -> MissingAddresses() + is WalletNotification.RateApp -> HowDoYouLikeTangem() + is WalletNotification.Critical.BackupError -> BackupError() + is WalletNotification.NoteMigration -> NotePromo() is WalletNotification.SwapPromo -> NoticePromotionBanner( source = AnalyticsParam.ScreensSources.Main, program = Program.Empty, // Use it on new promo action @@ -92,16 +93,17 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( WalletActivationBannerType.Attention -> AnalyticsParam.EmptyFull.Empty WalletActivationBannerType.Warning -> AnalyticsParam.EmptyFull.Full } - MainScreen.NoticeFinishActivation( + NoticeFinishActivation( activationState = activationState, balanceState = balanceState, ) } - is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport() - is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond() - is WalletNotification.PushNotifications -> WalletScreenAnalyticsEvent.PushBannerPromo.PushBanner() + is WalletNotification.Critical.SeedPhraseNotification -> NoticeSeedPhraseSupport() + is WalletNotification.Critical.SeedPhraseSecondNotification -> NoticeSeedPhraseSupportSecond() + is WalletNotification.PushNotifications -> PushBanner() is WalletNotification.Warning.TangemPayRefreshNeeded -> null - WalletNotification.Warning.TangemPayUnreachable -> null + is WalletNotification.Warning.TangemPayUnreachable -> null + is WalletNotification.UpgradeHotWalletPromo -> null } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index fe3861e0c2..a243ddef48 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -13,7 +13,10 @@ import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase +import com.tangem.domain.hotwallet.GetUpgradeBannerClosureTimestampUseCase +import com.tangem.domain.hotwallet.ShouldShowUpgradeHotWalletBannerUseCase import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrency @@ -43,6 +46,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map + import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -58,9 +62,12 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val notificationsRepository: NotificationsRepository, private val accountDependencies: AccountDependencies, private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, + private val shouldShowUpgradeHotWalletBannerUseCase: ShouldShowUpgradeHotWalletBannerUseCase, + private val getUpgradeBannerClosureTimestampUseCase: GetUpgradeBannerClosureTimestampUseCase, + private val checkHotWalletUpgradeBannerUseCase: CheckHotWalletUpgradeBannerUseCase, ) { - @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod") + @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod", "CastNullableToNonNullableType") fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver @@ -98,8 +105,12 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(), shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.YieldPromo) .distinctUntilChanged(), + shouldShowUpgradeHotWalletBannerUseCase.invoke(userWallet.walletId) + .distinctUntilChanged(), + getUpgradeBannerClosureTimestampUseCase(userWallet.walletId) + .distinctUntilChanged(), ) { array -> array } - .combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = array) } + .combine(tokenListFlow()) { array, any: Any? -> arrayOf(any).plus(elements = array) } .map { array -> val lceTokens = array[0] as Lce>> val totalFiatBalance = lceTokens.map { it.first } @@ -111,12 +122,22 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val shouldShowEnablePushesReminderNotification = array[5] as Boolean val shouldAccessCodeSkipped = array[6] as Boolean val shouldShowYieldPromo = array[7] as Boolean + val shouldShowUpgradeBanner = array[8] as Boolean + val closureTimestamp = array[9] as? Long buildList { addUsedOutdatedDataNotification(totalFiatBalance) addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) + addUpgradeHotWalletPromoNotification( + userWallet = userWallet, + flattenCurrencies = flattenCurrencies, + clickIntents = clickIntents, + shouldShowUpgradeBanner = shouldShowUpgradeBanner, + closureTimestamp = closureTimestamp, + ) + addFinishWalletActivationNotification( userWallet = userWallet, flattenCurrencies = flattenCurrencies, @@ -182,7 +203,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, ) @@ -449,6 +470,34 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } + private suspend fun MutableList.addUpgradeHotWalletPromoNotification( + userWallet: UserWallet, + flattenCurrencies: Lce>, + clickIntents: WalletClickIntents, + shouldShowUpgradeBanner: Boolean, + closureTimestamp: Long?, + ) { + if (userWallet !is UserWallet.Hot) return + + val currencies = flattenCurrencies.getOrNull(isPartialContentAccepted = true).orEmpty() + val hasBalance = currencies.any { it.value.amount.orZero().isPositive() } + + val shouldShow = checkHotWalletUpgradeBannerUseCase( + walletId = userWallet.walletId, + hasBalance = hasBalance, + shouldShowUpgradeBanner = shouldShowUpgradeBanner, + closureTimestamp = closureTimestamp, + ).getOrNull() ?: return + + addIf( + element = WalletNotification.UpgradeHotWalletPromo( + onLaterClick = { clickIntents.onCloseUpgradeBannerClick(userWallet.walletId) }, + onUpgradeClick = { clickIntents.onUpgradeHotWalletClick(userWallet.walletId) }, + ), + condition = shouldShow, + ) + } + private companion object { const val MAX_REMAINING_SIGNATURES_COUNT = 10 } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt index 38197dbb07..c7fe144bd9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt @@ -241,10 +241,11 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( } } - private fun getAccountStatusFlow(userWallet: UserWallet): Flow { + private fun getAccountStatusFlow(userWallet: UserWallet): Flow { val accountId = AccountId.forMainCryptoPortfolio(userWalletId = userWallet.walletId) return singleAccountStatusSupplier(SingleAccountStatusProducer.Params(accountId)) + .filterIsInstance() .distinctUntilChanged() .conflate() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/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/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt index 27856a0954..ae764cf797 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -251,7 +251,7 @@ internal enum class Wallet2CobrandImage( Sakura( cards2ResId = R.drawable.ill_sakura_card2_120_106, cards3ResId = R.drawable.ill_sakura_card3_120_106, - batchIds = setOf("AF990029", "AF990030", "AF990031"), + batchIds = setOf("AF990029", "AF990030", "AF990031", "AF990071", "AF990072", "AF990073"), ), SatoshiFriends( 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 bced5a1f46..2c52604748 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 @@ -26,10 +24,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) { @@ -52,12 +48,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/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 9abf719a45..ed1601ae6a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -445,4 +445,22 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ), ) + + data class UpgradeHotWalletPromo( + val onLaterClick: () -> Unit, + val onUpgradeClick: () -> Unit, + ) : WalletNotification( + config = NotificationConfig( + title = resourceReference(R.string.hw_upgrade_to_cold_banner_title), + subtitle = resourceReference(R.string.hw_upgrade_to_cold_banner_description), + iconResId = R.drawable.img_tangem_wallet_72, + buttonsState = ButtonsState.PairButtonsConfig( + primaryText = resourceReference(R.string.hw_upgrade), + onPrimaryClick = onUpgradeClick, + secondaryText = resourceReference(R.string.common_later), + onSecondaryClick = onLaterClick, + ), + iconSize = 72.dp, + ), + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/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/ReorderWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReorderWalletsTransformer.kt new file mode 100644 index 0000000000..334c41892c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReorderWalletsTransformer.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState +import kotlinx.collections.immutable.toImmutableList + +/** + * Transformer that reorders wallets according to the new order + * + * @property wallets wallets in the new order + */ +internal class ReorderWalletsTransformer( + private val wallets: List, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState { + val walletIdToIndex = wallets.withIndex().associate { it.value.walletId to it.index } + + val reorderedWallets = prevState.wallets + .sortedBy { walletIdToIndex[it.walletCardState.id] ?: Int.MAX_VALUE } + .toImmutableList() + + return if (reorderedWallets != prevState.wallets) { + prevState.copy(wallets = reorderedWallets) + } else { + prevState + } + } +} \ No newline at end of file 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/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 1a6b19c186..1847ff842f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -14,6 +14,7 @@ import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.tokenlist.TokenList @@ -144,6 +145,7 @@ internal class TokenListStateConverter( } val accountItems = accountList.accountStatuses + .filterCryptoPortfolio() .map { accountStatus -> when (accountStatus) { is AccountStatus.CryptoPortfolio -> accountStatus.map() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/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/BasicWalletSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicWalletSubscriber.kt index 585a48c0b0..b169aba32c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicWalletSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicWalletSubscriber.kt @@ -4,6 +4,7 @@ import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import kotlinx.coroutines.flow.Flow @@ -45,11 +46,9 @@ internal abstract class BasicWalletSubscriber : WalletSubscriber() { protected fun getMainAccountStatusFlow(): Flow { return getAccountStatusListFlow() .mapNotNull { accountStatusList -> - accountStatusList.accountStatuses.find { accountStatus -> - when (accountStatus) { - is AccountStatus.CryptoPortfolio -> accountStatus.account.isMainAccount - } - } as? AccountStatus.CryptoPortfolio + accountStatusList.accountStatuses + .filterCryptoPortfolio() + .find { accountStatus -> accountStatus.account.isMainAccount } } .distinctUntilChanged() .conflate() diff --git a/features/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/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index d8b52ad1ad..90fc34c41f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier import com.tangem.core.ui.components.notifications.NoteMigrationNotification import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.res.ForceDarkTheme import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import kotlinx.collections.immutable.ImmutableList @@ -40,6 +41,14 @@ internal fun LazyListScope.notifications(configs: ImmutableList { + ForceDarkTheme { + Notification( + config = item.config, + modifier = modifier.animateItem(fadeInSpec = null, fadeOutSpec = null), + ) + } + } else -> { Notification( config = item.config, 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/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index f9fefde39c..d686883940 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -27,7 +27,6 @@ import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus -import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -105,7 +104,7 @@ internal class WcPairModel @Inject constructor( private val selectedUserWalletFlow: MutableStateFlow by lazy { MutableStateFlow(getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }) } - private val selectedPortfolio = MutableSharedFlow>( + private val selectedPortfolio = MutableSharedFlow>( replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) @@ -171,7 +170,7 @@ internal class WcPairModel @Inject constructor( private suspend fun handlePairState( pairState: WcPairState, portfolios: PortfolioFetcher.Data? = null, - selected: Pair? = null, + selected: Pair? = null, isAccountMode: Boolean? = null, ) { when (pairState) { @@ -206,7 +205,7 @@ internal class WcPairModel @Inject constructor( private suspend fun handleProposalState( pairState: WcPairState.Proposal, portfolios: PortfolioFetcher.Data? = null, - selected: Pair? = null, + selected: Pair? = null, ) { val availableWallets = pairState.dAppSession.proposalNetwork.keys .filter { !it.isLocked && it.isMultiCurrency } @@ -266,7 +265,7 @@ internal class WcPairModel @Inject constructor( } private suspend fun tryToCreatePortfolioSelectRow( - selectedPortfolio: Pair?, + selectedPortfolio: Pair?, portfolios: PortfolioFetcher.Data?, ): PortfolioSelectUM? { selectedPortfolio ?: return null @@ -274,7 +273,6 @@ internal class WcPairModel @Inject constructor( val (wallet, portfolioAccount) = selectedPortfolio val account = when (val account = portfolioAccount.account) { is Account.CryptoPortfolio -> account - is Account.Payment -> TODO("[REDACTED_JIRA]") } val isAccountMode = selectorController.isAccountMode.first() val icon: AccountIconUM.CryptoPortfolio? diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsAccountModeTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsAccountModeTransformer.kt index fa672da841..f3991ee130 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsAccountModeTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsAccountModeTransformer.kt @@ -48,7 +48,7 @@ internal class WcSessionsAccountModeTransformer( ) items.add(walletHeader) - accountList.accounts.forEach accountsForEach@{ account -> + accountList.accounts.filterIsInstance().forEach accountsForEach@{ account -> val accountSessions = sessions.filter { it.account?.accountId == account.accountId } if (accountSessions.isEmpty()) return@accountsForEach val connectedApps = accountSessions.map { dappSession -> @@ -65,7 +65,6 @@ internal class WcSessionsAccountModeTransformer( } val accountIcon = when (account) { is Account.CryptoPortfolio -> account.icon - is Account.Payment -> TODO("[REDACTED_JIRA]") } val accountTitle = AccountTitleUM.Account( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcPortfolioNameDelegate.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcPortfolioNameDelegate.kt index 5df48f4e4d..cad34dfe8e 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcPortfolioNameDelegate.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/converter/WcPortfolioNameDelegate.kt @@ -19,13 +19,12 @@ internal class WcPortfolioNameDelegate @AssistedInject constructor( isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, @Assisted private val scope: CoroutineScope, ) { - val isAccountMode = isAccountsModeEnabledUseCase.invoke() + private val isAccountMode = isAccountsModeEnabledUseCase.invoke() .stateIn(scope = scope, started = SharingStarted.Eagerly, initialValue = false) fun createAccountTitleUM(value: WcSession): AccountTitleUM? { val account = when (val account = value.account) { is Account.CryptoPortfolio -> account - is Account.Payment -> TODO("[REDACTED_JIRA]") null -> null } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt index 7f74dc7dca..11fba2839f 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt @@ -11,7 +11,6 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.WcRequestUseCaseFactory import com.tangem.domain.walletconnect.usecase.method.WcAddNetworkUseCase diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 9eaceb4978..a156c5a69c 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -23,7 +23,6 @@ import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.core.lce.Lce -import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt index 1e97be37f4..7a0a21fe7f 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt @@ -12,7 +12,6 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.WcRequestUseCaseFactory import com.tangem.domain.walletconnect.model.WcEthMethod diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt index 199cc48016..8f5a202a76 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/common/WcTransactionRequestButtons.kt @@ -12,7 +12,6 @@ import com.tangem.core.ui.components.HoldToConfirmButton import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButtonIconEnd import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.R as CoreR import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe @@ -55,10 +54,7 @@ internal fun WcTransactionRequestButtons( isHoldToConfirmEnabled -> { HoldToConfirmButton( modifier = buttonModifier, - text = stringResourceSafe( - CoreR.string.common_hold_to, - activeButtonText.resolveReference(), - ), + text = activeButtonText.resolveReference(), onConfirm = onClickActiveButton, isLoading = isLoading, enabled = enabled, diff --git a/features/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..5c16293dd7 100644 --- a/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt +++ b/features/welcome/impl/src/main/kotlin/com/tangem/features/welcome/impl/model/WelcomeModel.kt @@ -192,6 +192,7 @@ internal class WelcomeModel @Inject constructor( modelScope.launch { scanCardProcessor.scan( analyticsSource = AnalyticsParam.ScreensSources.SignIn, + shouldCheckIsAlreadyActivated = true, onWalletNotCreated = {}, disclaimerWillShow = { router.pop() }, onSuccess = { scanResponse -> @@ -257,7 +258,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/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyAlertFactory.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyAlertFactory.kt index 00958d2ec1..e5f5e68cca 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyAlertFactory.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyAlertFactory.kt @@ -32,10 +32,13 @@ class YieldSupplyAlertFactory @Inject constructor( title = resourceReference(id = R.string.send_alert_transaction_failed_title), message = resourceReference(id = R.string.common_unknown_error), onDismissRequest = popBack, - firstAction = EventMessageAction( - title = resourceReference(R.string.common_support), - onClick = onFailedTxEmailClick, - ), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_support), + onClick = onFailedTxEmailClick, + ) + }, + secondActionBuilder = { cancelAction { } }, ), ) } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/YieldSupplyApproveComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/YieldSupplyApproveComponent.kt index f56edfc633..dc2ffd7777 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/YieldSupplyApproveComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/YieldSupplyApproveComponent.kt @@ -20,7 +20,6 @@ import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.HoldToConfirmButton import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.R as CoreR import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle @@ -82,10 +81,7 @@ internal class YieldSupplyApproveComponent( if (state.isHoldToConfirmEnabled) { HoldToConfirmButton( - text = stringResourceSafe( - CoreR.string.common_hold_to, - stringResourceSafe(R.string.common_confirm), - ), + text = stringResourceSafe(R.string.common_confirm), onConfirm = model::onClick, enabled = state.isPrimaryButtonEnabled, isLoading = state.isTransactionSending, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt index c7957a5d41..c561bab7ca 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/YieldSupplyStartEarningComponent.kt @@ -26,7 +26,6 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.core.ui.R as CoreR import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyActionUM import com.tangem.features.yield.supply.impl.common.entity.YieldSupplyFeeUM @@ -114,10 +113,7 @@ internal class YieldSupplyStartEarningComponent( if (state.isHoldToConfirmEnabled) { HoldToConfirmButton( - text = stringResourceSafe( - CoreR.string.common_hold_to, - stringResourceSafe(R.string.yield_module_start_earning), - ), + text = stringResourceSafe(R.string.yield_module_start_earning), onConfirm = model::onClick, enabled = state.isPrimaryButtonEnabled, isLoading = state.isTransactionSending, diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt index eae26ea8e1..58c0a5e000 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/YieldSupplyStopEarningComponent.kt @@ -21,7 +21,6 @@ import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.HoldToConfirmButton import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.R as CoreR import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle @@ -86,10 +85,7 @@ internal class YieldSupplyStopEarningComponent( if (state.isHoldToConfirmEnabled) { HoldToConfirmButton( - text = stringResourceSafe( - CoreR.string.common_hold_to, - stringResourceSafe(R.string.common_confirm), - ), + text = stringResourceSafe(R.string.common_confirm), onConfirm = model::onClick, enabled = state.isPrimaryButtonEnabled, isLoading = state.isTransactionSending, diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index eba16c785f..d56c94178f 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.33-1423" +tangemBlockchainSdk = "releases-5.34-1430" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.33-576" +tangemCardSdk = "releases-5.34-587" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-545" +tangemHotSdk = "develop-539" #tangemHotSdk = "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/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index 64016c2cf8..a7697062bd 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -45,10 +45,18 @@ interface TangemSdkManager { suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean = true): Boolean + /** + * Scans the card and returns [ScanResponse] with the card information. + * If [cardId] is provided, it will try to find the card with this ID in the card reader's range. + * If [allowsRequestAccessCodeFromRepository] is true, it will try to request access code from the repository + * if the card is protected by access code and the access code is not saved in the repository. + * If [shouldCheckIsAlreadyActivated] is true, it will check if the card is already activated and return an error if it is. + */ suspend fun scanProduct( cardId: String? = null, messageRes: Int? = null, allowsRequestAccessCodeFromRepository: Boolean = false, + shouldCheckIsAlreadyActivated: Boolean, ): CompletionResult suspend fun createProductWallet( 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..43fab6f690 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit dc9df17919126bce93482559fe26602f9801eb42 +Subproject commit 43fab6f690538391cae17e046ffb2ec9fe08b0c7 diff --git a/test/mock/src/main/java/com/tangem/test/mock/MockAccounts.kt b/test/mock/src/main/java/com/tangem/test/mock/MockAccounts.kt index f275e2b0d2..d2bdca9cb0 100644 --- a/test/mock/src/main/java/com/tangem/test/mock/MockAccounts.kt +++ b/test/mock/src/main/java/com/tangem/test/mock/MockAccounts.kt @@ -46,7 +46,7 @@ object MockAccounts { derivationIndex: Int, name: String = "Account #$derivationIndex", icon: CryptoPortfolioIcon = CryptoPortfolioIcon.ofDefaultCustomAccount(), - cryptoCurrencies: Set = emptySet(), + cryptoCurrencies: List = emptyList(), userWalletId: UserWalletId = this.userWalletId, ): Account.CryptoPortfolio { val derivationIndex = DerivationIndex(derivationIndex).getOrNull()!!